diff --git a/0x0026a_custom-bootloader/0x0026a_full.uf2 b/0x0026a_custom-bootloader/0x0026a_full.uf2 new file mode 100644 index 0000000..5215266 Binary files /dev/null and b/0x0026a_custom-bootloader/0x0026a_full.uf2 differ diff --git a/0x0026a_custom-bootloader/CMakeLists.txt b/0x0026a_custom-bootloader/CMakeLists.txt index 82c837e..299af94 100644 --- a/0x0026a_custom-bootloader/CMakeLists.txt +++ b/0x0026a_custom-bootloader/CMakeLists.txt @@ -81,8 +81,9 @@ pico_enable_stdio_usb(0x0026a_app 0) pico_set_linker_script(0x0026a_app ${CMAKE_CURRENT_LIST_DIR}/app/memmap_app.ld) -# The application starts at 0x10020000, so a UF2 (which BOOTSEL always writes to -# 0x10000000) would be wrong. Emit the raw artifacts we flash with OpenOCD. +# The application starts at 0x10020000. A standalone app UF2 would flash only +# this one region, so the lab ships a single combined UF2 instead (built by +# tools/make_full_uf2.py). Emit the raw artifacts we sign and flash with OpenOCD. pico_add_dis_output(0x0026a_app) pico_add_hex_output(0x0026a_app) pico_add_bin_output(0x0026a_app) diff --git a/0x0026a_custom-bootloader/README.md b/0x0026a_custom-bootloader/README.md index b5b0cf6..fc4d7f0 100644 --- a/0x0026a_custom-bootloader/README.md +++ b/0x0026a_custom-bootloader/README.md @@ -24,13 +24,14 @@ The CMSIS-RM9 flash-safe range starts at `0x10000000` on the RP2350, so adding 2 ## Artifacts -| File | Description | -| ---------------------------- | ---------------------------------------- | -| `0x0026a_bootloader.bin` | Bootloader image (flash at `0x10000000`) | -| `0x0026a_bootloader.uf2` | Bootloader for the BOOTSEL drag-and-drop | -| `0x0026a_app.bin` | Signed app image (flash at `0x10020000`) | -| `manifest.bin` | Manifest (flash at `0x1001F000`) | -| `WEEK11a.md` / `WEEK11a.pdf` | Full lab notebook | +| File | Description | +| ---------------------------- | ------------------------------------------------- | +| `0x0026a_full.uf2` | **All three images** in one BOOTSEL drag | +| `0x0026a_bootloader.bin` | Bootloader image (flash at `0x10000000`) | +| `0x0026a_bootloader.uf2` | Bootloader-only UF2 for the BOOTSEL drag-and-drop | +| `0x0026a_app.bin` | Signed app image (flash at `0x10020000`) | +| `manifest.bin` | Manifest (flash at `0x1001F000`) | +| `WEEK11a.md` / `WEEK11a.pdf` | Full lab notebook | ## Build @@ -38,12 +39,20 @@ The CMSIS-RM9 flash-safe range starts at `0x10000000` on the RP2350, so adding 2 cmake -B build -DPICO_SDK_PATH=~/.pico-sdk/sdk/2.2.0 cmake --build build python3 tools/sign_firmware.py build/0x0026a_app.bin --out manifest.bin --key "IronNetBootKey!!" --app-base 0x10020000 +PICOTOOL=~/.pico-sdk/picotool/2.3.1/picotool/picotool python3 tools/make_full_uf2.py ``` -This produces `0x0026a_bootloader.bin/.uf2` and `0x0026a_app.bin`; the signer writes `manifest.bin`. +This produces `0x0026a_bootloader.bin/.uf2` and `0x0026a_app.bin`; the signer +writes `manifest.bin`; `make_full_uf2.py` packs all three into +`0x0026a_full.uf2`. ## Flash +Easiest - copy `0x0026a_full.uf2` to the `RP2350` BOOTSEL drive; one drag writes +the whole chain (bootloader + manifest + app). + +Or with OpenOCD: + ```sh openocd -f interface/picoprobe.cfg -f target/rp2350.cfg -c \ "program 0x0026a_bootloader.bin 0x10000000 verify; \ diff --git a/0x0026a_custom-bootloader/WEEK11a.md b/0x0026a_custom-bootloader/WEEK11a.md index 25493c1..dd591d4 100644 --- a/0x0026a_custom-bootloader/WEEK11a.md +++ b/0x0026a_custom-bootloader/WEEK11a.md @@ -519,15 +519,31 @@ $ ~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-nm -n build/0x0026a_app.elf ... ``` -### 6.9 Why the Application Has No .uf2 +### 6.9 One UF2 for the Whole Chain -A UF2 file writes to the address encoded in each of its blocks, and the SDK's -UF2 tooling always targets `0x10000000`. If a user holds BOOTSEL and drags in -`0x0026a_app.uf2`, the app would be written at the wrong base and the -bootloader would jump into garbage (or nothing). So the app is deliberately -built **without** a UF2 and with explicit `.bin`/`.hex`/`.dis`/`.map` outputs -instead. The **bootloader** still ships a UF2 (it belongs at `0x10000000`), and -the manifest + app are written to their own addresses with OpenOCD. +No single image is the whole story. The device needs the bootloader at +`0x10000000`, the manifest at `0x1001F000`, and the app at `0x10020000`. A +standalone `0x0026a_app.uf2` would flash only the app region, leaving the other +two to be written separately, so the app is deliberately built with explicit +`.bin`/`.hex`/`.dis`/`.map` outputs instead of its own UF2. + +To bring a bare board up in one move the lab ships a single combined UF2, +`0x0026a_full.uf2`, that carries all three images. It is built by laying the +images out flat - offset 0 mapped to `0x10000000`, the reserved gaps filled +with `0xFF` - and handing the result to picotool as one image: + +``` +$ PICOTOOL=~/.pico-sdk/picotool/2.3.1/picotool/picotool \ + python3 tools/make_full_uf2.py +``` + +picotool numbers the blocks, tags them with the `rp2350-arm-s` family, and +injects the RP2350-E10 "absolute block" (family `0xE48BFF57` at `0x10FFFF00`) +that tells the boot ROM to write every block to its **absolute** address rather +than into a partition. A single BOOTSEL drag then restores the whole chain. +Part 11 uses it. Note the contrast with the naive file layout: a UF2 is not +restricted to `0x10000000`, but a multi-region UF2 is only correct once the +blocks and the E10 absolute block are laid out the way picotool produces them. --- @@ -974,11 +990,27 @@ the manifest, then the app. If you flash the app before the manifest, the bootloader happily boots it anyway (the manifest is already in flash from a previous lab run) - order only matters on a bare board. +### The whole chain in one drag (recommended) + +On a bare board skip straight to one file: hold BOOTSEL, connect the Pico 2, +and copy `0x0026a_full.uf2` to the `RP2350` drive. That single UF2 writes all +three images at once - no OpenOCD, no second drag. Section 6.9 explains how it +is built; the hardware-verified behavior is: + +- BOOTSEL accepts it and unmounts the drive; +- the boot ROM writes bootloader, manifest, and app to their absolute + addresses; +- on reset the bootloader verifies the manifest and jumps to the app. + +The steps below are the three-image equivalent, which is what you use while +iterating on a single image or when you prefer OpenOCD over the button. + ### A. The bootloader: two options **Option 1 - BOOTSEL (no extra tools):** hold the BOOTSEL button, connect the -Pico 2, copy `0x0026a_bootloader.uf2` to the drive. BOOTSEL always writes at -`0x10000000`, which is correct for the bootloader. +Pico 2, copy `0x0026a_bootloader.uf2` to the drive, which lands the bootloader +at `0x10000000` (or drag `0x0026a_full.uf2` to write the whole chain in one +shot). **Option 2 - OpenOCD:** @@ -1107,9 +1139,10 @@ the bootloader refuses. ### Step 13: Confirm it still boots after restoring the good image -Re-flash `0x0026a_app.bin` (fresh, matching the manifest) and reset. The -banner returns. The bootloader is doing real enforcement, not just complaining -once. +Re-flash `0x0026a_app.bin` (fresh, matching the manifest) and reset - or just +drag `0x0026a_full.uf2` onto the BOOTSEL drive again to restore the whole +chain in one move. The banner returns. The bootloader is doing real +enforcement, not just complaining once. Stop and think about the security statement this demonstrated: diff --git a/0x0026a_custom-bootloader/WEEK11a.pdf b/0x0026a_custom-bootloader/WEEK11a.pdf index 1de8ec0..8eb4f81 100644 Binary files a/0x0026a_custom-bootloader/WEEK11a.pdf and b/0x0026a_custom-bootloader/WEEK11a.pdf differ diff --git a/0x0026a_custom-bootloader/tools/make_full_uf2.py b/0x0026a_custom-bootloader/tools/make_full_uf2.py new file mode 100755 index 0000000..bf65bea --- /dev/null +++ b/0x0026a_custom-bootloader/tools/make_full_uf2.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Build a single BOOTSEL-draggable UF2 holding the whole secure-boot chain. + +The 0x0026a chain is three separate images at three flash addresses: + + bootloader 0x10000000 (owns the system, verifies the next image) + manifest 0x1001F000 (header + AES-128-CMAC tag) + app 0x10020000 (the signed application) + +The SDK emits a UF2 only for the bootloader, so a bare board would need three +separate flashes. This tool packs all three into ONE UF2 so a single BOOTSEL +drag-and-drop brings the whole device up. + +How it works: + + 1. Lay the three images out in a flat buffer whose offset 0 maps to + 0x10000000, filling the reserved gap between them with 0xFF (erased + flash). + 2. Hand that flat image to picotool as a normal image at 0x10000000. + 3. picotool numbers the blocks, tags them with the rp2350-arm-s family and + injects the RP2350-E10 absolute block that makes the boot ROM write each + block to its absolute address. + +The result is exactly the shape the boot ROM accepts over the BOOTSEL mass +storage interface, and it has been verified on hardware: the ROM accepts the +drag, the bootloader verifies the manifest, and the app runs. +""" + +import argparse +import hashlib +import os +import struct +import subprocess +import sys +import tempfile + +BOOTLOADER_BASE = 0x10000000 +MANIFEST_BASE = 0x1001F000 +APP_BASE = 0x10020000 +FAMILY = "rp2350-arm-s" +PLATFORM = "rp2350" + + +def build_flat(bootloader, manifest, app): + """Lay the three images out flat, offset 0 mapped to 0x10000000.""" + if APP_BASE < MANIFEST_BASE < BOOTLOADER_BASE: + raise SystemExit("address map is not ordered") + size = (APP_BASE - BOOTLOADER_BASE) + len(app) + image = bytearray(b"\xFF" * size) + image[0:len(bootloader)] = bootloader + m_off = MANIFEST_BASE - BOOTLOADER_BASE + image[m_off:m_off + len(manifest)] = manifest + a_off = APP_BASE - BOOTLOADER_BASE + image[a_off:a_off + len(app)] = app + return bytes(image) + + +def convert(flat, out, picotool): + """Run picotool to turn the flat image into an absolute-address UF2.""" + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as tmp: + tmp.write(flat) + flat_path = tmp.name + try: + cmd = [ + picotool, "uf2", "convert", flat_path, out, + "-o", hex(BOOTLOADER_BASE), + "--family", FAMILY, + "--platform", PLATFORM, + "--abs-block", + ] + subprocess.run(cmd, check=True) + finally: + os.unlink(flat_path) + + +def verify(out): + """Report the block count and the families present in the UF2.""" + data = open(out, "rb").read() + blocks = [data[i:i + 512] for i in range(0, len(data), 512)] + families = set() + payload = 0 + for blk in blocks: + if len(blk) != 512 or struct.unpack_from("