diff --git a/0x0026a_custom-bootloader/.gitignore b/0x0026a_custom-bootloader/.gitignore new file mode 100644 index 0000000..cd1e73d --- /dev/null +++ b/0x0026a_custom-bootloader/.gitignore @@ -0,0 +1,2 @@ +build +!.vscode/* \ No newline at end of file diff --git a/0x0026a_custom-bootloader/0x0026a_app.bin b/0x0026a_custom-bootloader/0x0026a_app.bin new file mode 100755 index 0000000..f6c83d6 Binary files /dev/null and b/0x0026a_custom-bootloader/0x0026a_app.bin differ diff --git a/0x0026a_custom-bootloader/0x0026a_bootloader.bin b/0x0026a_custom-bootloader/0x0026a_bootloader.bin new file mode 100755 index 0000000..5fd7c1b Binary files /dev/null and b/0x0026a_custom-bootloader/0x0026a_bootloader.bin differ diff --git a/0x0026a_custom-bootloader/0x0026a_bootloader.uf2 b/0x0026a_custom-bootloader/0x0026a_bootloader.uf2 new file mode 100644 index 0000000..dc84248 Binary files /dev/null and b/0x0026a_custom-bootloader/0x0026a_bootloader.uf2 differ diff --git a/0x0026a_custom-bootloader/CMakeLists.txt b/0x0026a_custom-bootloader/CMakeLists.txt new file mode 100644 index 0000000..82c837e --- /dev/null +++ b/0x0026a_custom-bootloader/CMakeLists.txt @@ -0,0 +1,89 @@ +# 0x0026a custom bootloader: builds a secure bootloader plus the signed +# application it launches. Run from this directory: +# +# cmake -B build -G Ninja -DPICO_BOARD=pico2 \ +# -DPICO_SDK_PATH="$HOME/.pico-sdk/sdk/2.2.0" \ +# -DPICO_TOOLCHAIN_PATH="$HOME/.pico-sdk/toolchain/14_2_Rel1" +# cmake --build build + +cmake_minimum_required(VERSION 3.13) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work == +if(WIN32) + set(USERHOME $ENV{USERPROFILE}) +else() + set(USERHOME $ENV{HOME}) +endif() +set(sdkVersion 2.2.0) +set(toolchainVersion 14_2_Rel1) +set(picotoolVersion 2.2.0-a4) +set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake) +if (EXISTS ${picoVscode}) + include(${picoVscode}) +endif() +# ==================================================================================== +set(PICO_BOARD pico2 CACHE STRING "Board type") + +# Pull in Raspberry Pi Pico SDK (must be before project) +include(pico_sdk_import.cmake) + +project(0x0026a_custom_bootloader C CXX ASM) + +# Initialise the Raspberry Pi Pico SDK +pico_sdk_init() + +# --- Bootloader: verifies an AES-128-CMAC tag before launching the app -------- +add_executable(0x0026a_bootloader + bootloader/src/main.c + bootloader/src/boot.c + bootloader/src/manifest.c + bootloader/src/aes_cmac.c + bootloader/src/image.c + bootloader/src/verify.c) + +target_include_directories(0x0026a_bootloader PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/bootloader/include + ${CMAKE_CURRENT_LIST_DIR}/common/include) + +# pico_mbedtls provides the AES-128-CMAC implementation used for verification. +target_link_libraries(0x0026a_bootloader PRIVATE + pico_stdlib + pico_mbedtls) + +pico_enable_stdio_uart(0x0026a_bootloader 1) +pico_enable_stdio_usb(0x0026a_bootloader 0) +pico_add_extra_outputs(0x0026a_bootloader) + +# --- Application: the 0x0026_functions firmware linked at 0x10020000 ---------- +add_executable(0x0026a_app + app/src/app_main.c + app/src/ir.c + app/src/led.c + app/src/secret.c) + +target_include_directories(0x0026a_app PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/app/include + ${CMAKE_CURRENT_LIST_DIR}/common/include) + +# pico_mbedtls provides the AES-128-CBC API used by secret.c. +target_link_libraries(0x0026a_app PRIVATE + pico_stdlib + pico_mbedtls) + +pico_enable_stdio_uart(0x0026a_app 1) +pico_enable_stdio_usb(0x0026a_app 0) + +# Relocate the application into flash above the bootloader and manifest. +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. +pico_add_dis_output(0x0026a_app) +pico_add_hex_output(0x0026a_app) +pico_add_bin_output(0x0026a_app) +pico_add_map_output(0x0026a_app) diff --git a/0x0026a_custom-bootloader/README.md b/0x0026a_custom-bootloader/README.md new file mode 100644 index 0000000..b5b0cf6 --- /dev/null +++ b/0x0026a_custom-bootloader/README.md @@ -0,0 +1,60 @@ +# 0x0026a Custom AES-128-CMAC Secure Bootloader + +This project builds a custom secure-boot chain for the Pico 2 (RP2350) that sits entirely above the ROM bootloader. A first-stage bootloader in XIP flash authenticates a signed application with AES-128-CMAC (RFC 4493) before jumping to it, and the signed application decrypts and prints a secret banner using the SDK Mbed TLS AES API. + +## What You'll Learn + +- Why a bootloader-only CRC/manifest is insufficient and what a MAC adds +- How AES-128-CMAC works (K1/K2 subkeys) and the RFC 4493 test vectors +- Custom linker-script editing (`pico_flash_region.ld`, `memmap_app.ld`) to relocate the app +- Flashing three images (bootloader, manifest, app) with OpenOCD +- Reverse engineering both images with GDB and Ghidra (find the key, IV, ciphertext, banner) + +See the full tutorial: [WEEK11a.md](WEEK11a.md) / [WEEK11a.pdf](WEEK11a.pdf) + +## Flash Map + +| Region | Address | Content | +| ---------- | ----------- | ------------------------------ | +| Bootloader | `0x10000000` | 0x0026a bootloader (27772 B) | +| Manifest | `0x1001F000` | header + AES-128-CMAC tag | +| App | `0x10020000` | 0x0026a signed app (21656 B) | + +The CMSIS-RM9 flash-safe range starts at `0x10000000` on the RP2350, so adding 2 MB relocates the app to `0x10020000`. + +## 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 | + +## Build + +```sh +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 +``` + +This produces `0x0026a_bootloader.bin/.uf2` and `0x0026a_app.bin`; the signer writes `manifest.bin`. + +## Flash + +```sh +openocd -f interface/picoprobe.cfg -f target/rp2350.cfg -c \ + "program 0x0026a_bootloader.bin 0x10000000 verify; \ + program manifest.bin 0x1001F000 verify; \ + program 0x0026a_app.bin 0x10020000 verify; reset run; shutdown" +``` + +## Verify at Runtime + +Halt the core after boot and read the program counter. A valid signed image leaves PC inside the app (`0x100201xx`); a tampered or unsigned image leaves PC in the bootloader `boot_halt` idle loop near `0x10000eb2`. + +## Disclaimers + +This is an educational project for the Embedded-Hacking course. The key `IronNetBootKey!!` ships in the repo and is intentionally embedded in both images so it can be recovered with GDB/Ghidra. Do not use this scheme to protect real products. \ No newline at end of file diff --git a/0x0026a_custom-bootloader/WEEK11a.md b/0x0026a_custom-bootloader/WEEK11a.md new file mode 100644 index 0000000..1268beb --- /dev/null +++ b/0x0026a_custom-bootloader/WEEK11a.md @@ -0,0 +1,1662 @@ +# Week 11a: Custom Secure Boot + AES in Firmware: Building a CAC-Signed Bootloader and Reverse Engineering It + +*** + +**LEGAL DISCLAIMER:** +The information, tools, and code provided in this repository and course are strictly for educational, research, and defensive purposes only. + +You are explicitly prohibited from using any materials contained herein to access, test, modify, or exploit any device, network, or system that you do not own 100% or for which you do not have explicit, documented, and legally binding authorization to interact with. + +By using this repository and course, you acknowledge and agree that: + +1. Any illegal, unauthorized, or malicious use of this information is solely your responsibility. +2. The author(s) and contributor(s) of this repository and course shall not be held liable for any damages, legal repercussions, criminal charges, or unauthorized actions resulting from the use, misuse, or abuse of the contents herein. +3. You will comply with all applicable local, state, national, and international laws regarding cybersecurity and computer fraud. + +**IF YOU DO NOT AGREE WITH THESE TERMS, DO NOT USE THIS REPOSITORY AND COURSE.** + +*** + +## What You'll Learn This Week + +By the end of this tutorial, you will be able to: + +- Understand why consumer and industrial devices use a secure (authenticated) boot chain +- Explain, with a diagram, how AES-128-CMAC (RFC 4493) authenticates a firmware image +- Read a custom linker script, modify the MEMORY block, and relocate a program to a higher flash address +- Inspect a relocated ELF with `readelf`, `nm`, and `objdump` to confirm where every section and symbol lives +- Use Mbed TLS (already shipped inside the Pico SDK) in your own C code to decrypt data at runtime +- Flash a raw `.bin`, a manifest, and an application to three different flash addresses with OpenOCD +- Tamper with a single byte of firmware and watch the bootloader reject it at boot +- Reverse engineer the bootloader with GDB live on the Pico 2 (breakpoints, memory dumps, single-stepping the jump) +- Reverse engineer both firmware images in Ghidra, including recovering an AES key, IV, and ciphertext from flash +- Explain why a static key is not a real secret, and how real products solve secure boot (OTP keys, ROM verification, ECDSA) + +--- + +## Part 1: Why Secure Boot? (The Threat Model) + +### Firmware is a Product, and Products Get Modified + +Everything you have flashed onto your Pico 2 so far in this course has been a **single standalone image**. The RP2350 boot ROM reads the image at `0x10000000`, your program runs, done. That is exactly how millions of cheap embedded products work, and it has a serious weakness: + +**Anybody with a $5 probe can replace the flash contents.** + +OpenOCD, a 5-pin SWD connection, and a serial terminal are all an attacker needs to: + +1. Dump your firmware (read flash) +2. Modify a byte (change a password, unlock a feature, bypass a license) +3. Flash their own image (rootkit the device) + +Now tighten the screw. Imagine your product is: + +- A smart lock that talks to the network +- A security camera that signs video +- A commercial battery charger that detects counterfeit batteries +- An update server that pushes "fixes" to thousands of deployed devices + +In those products, **running modified or unauthorized firmware is a real-world safety and business problem**. If a device can be made to run code it did not originate, that device cannot be trusted, and neither can the data it produces. + +### The Defense: A Verified Boot Chain + +The modern defense is a **boot chain**. Each stage of the chain refuses to start the next stage until it has proven (cryptographically) that the next stage is authentic: + +``` ++-----------------------------------------------------------------+ +| BOOT CHAIN (each stage authenticates the next) | +| | +| +-------------------------------------------------------------------+ +| | STAGE 0: ROM in the chip (frozen in silicon, cannot be changed) | +| | verifies... | +| +-------------------------------------------------------------------+ +| | trusts +| v +| +-------------------------------------------------------------------+ +| | STAGE 1: bootloader in flash (this lab) | +| | verifies the application with an AES-128-CMAC tag | +| +-------------------------------------------------------------------+ +| | trusts (only if the tag matches) +| v +| +-------------------------------------------------------------------+ +| | STAGE 2: application firmware (the IR remote control app) | +| +-------------------------------------------------------------------+ +| | +| An attacker's modified app is REJECTED at stage 1 and never | +| runs. The device is very hard to brick with bad firmware, and | +| cheap modification attacks fail. | ++-----------------------------------------------------------------+ +``` + +> **What this lab does, and what it does not.** +> +> The 0x0026a bootloader authenticates the application using a key that is +> **embedded in the bootloader itself**. That stops a casual attacker who uses +> OpenOCD to patch a byte or two. A serious attacker who dumps the flash can +> read the key and re-sign their own image. Real products push Stage 0 into +> the ROM and store the key in **OTP (one-time programmable) memory** that +> flash reads cannot access. Part 19 shows how to think about that gap. + +--- + +## Part 2: Choosing the Authentication Primitive + +The bootloader must answer one question about the application image: +"Did this byte-for-byte image come from the person who holds the key?" + +### The Candidate Tools + +| Tool | What it computes | Can an attacker forge it? | +| ------------- | ---------------------------------------- | ----------------------------------------------------------- | +| CRC-32 | A simple checksum | YES, trivially. CRC is designed to catch noise, not attackers. | +| SHA-256 hash | A one-way digest of the image | YES, if there is no secret. Anyone can compute the hash after editing. | +| HMAC-SHA256 | A keyed digest | NO (without the key). Strong, but needs a keyed hash implementation. | +| AES-CMAC | A keyed MAC built from a block cipher | NO (without the key). Uses the AES primitive you already have. | +| ECDSA signature | An asymmetric signature | NO. The verifier holds only a public key, so even a full flash dump cannot forge. | + +### Why AES-CMAC for This Lab + +Two practical reasons: + +1. **The Pico SDK already ships a fast, well-tested AES.** CMAC is built on AES + (it is a CBC-MAC variant), so we do not add SHA code or HMAC glue; we reuse + the `mbedtls` library that ships inside the SDK (`pico_mbedtls`). +2. **One primitive, two lessons.** The same AES engine that authenticates the + image in the bootloader is also used inside the application to decrypt a + secret banner (Part 9). You see one cipher doing two classic jobs. + +ECDSA would be stronger (asymmetric, so the flash dump does not reveal the +signing key), and the RP2350 ROM ships a `secp256k1` verifier. The gap between +where this lab leaves off and where the ROM verifier lies is an important +"real product" paragraph in Part 19. + +--- + +## Part 3: AES-128-CMAC in Depth (RFC 4493) + +### A MAC Is a Keyed Checksum + +A **Message Authentication Code (MAC)** takes three inputs: + +- a secret key `K` +- a message `M` +- (optionally) a length `Tlen` + +and produces a short, fixed-size **tag** `T`. Security property: without `K`, +an attacker cannot construct `(M, T)` that verifies, even if they are allowed +to see thousands of valid `(M, T)` pairs. + +### Outline of CMAC + +CMAC is a keyed block-cipher MAC standardised in **NIST SP 800-38B** and +implemented in **RFC 4493** for AES-128. It is a **CBC-MAC with a twist**: the +final block is XORed with one of two derived subkeys before the last AES +encryption, which removes the classical CBC-MAC extension attack. + +``` ++-----------------------------------------------------------------+ +| Subkey derivation (once, at key setup) | +| | +| L = AES_K(0^128) AES-encrypt a zero block | +| if MSB(L) == 0: K1 = L << 1 | +| else: K1 = (L << 1) XOR 0x87 (0x87 = Rb for AES) | +| same step on K1 yields K2 | +| | +| Tagging (per message M, split into 128-bit blocks M1..Mn) | +| | +| M0 = 0^128 | +| if M is an exact multiple of 128 bits: | +| Mn' = Mn XOR K1 | +| else: (pad M with 10...0 to a full block) | +| Mn' = padded(Mn) XOR K2 | +| | +| for each block: C[i] = AES_K( C[i-1] XOR M[i] ) | +| Tag = C[n] (first Tlen bits) | ++-----------------------------------------------------------------+ +``` + +The two subkeys exist precisely to encode "full last block" vs "short last +block" into the tag. If we reused plain CBC-MAC, an attacker could cut the +message after a block boundary and still produce a valid tag. + +### Where Is AES-128-CMAC Not Really "Analysis" But Simple Math? + +Study the equation for a single message block: + +``` +C[1] = AES_K( 0^128 XOR M1 ) +``` + +Because this is a block cipher, a 1-bit change anywhere in `M1` results in a +completely unrelated tag. And without `K`, you cannot compute +`AES_K(...)` at all. Both facts are what make the tag unforgeable in practice. + +### The RFC 4493 Test Vectors (how the tool proves itself) + +The host signer `tools/sign_firmware.py` implements AES and CMAC from scratch +in pure Python, so the build toolchain has zero extra dependency on the +development machine. It is validated against the four official test vectors +from RFC 4493 using the well-known test key: + +``` +Key = 2b7e151628aed2a6abf7158809cf4f3c + +Message = (empty) +Tag = bb1d6929e95937287fa37d129b756746 + +Message = 6bc1bee22e409f96e93d7e117393172a +Tag = 070a16b46b4d4144f79bdd9dd04a287c + +Message = 40-byte repeating sample +Tag = dfa66747de9ae63030ca32611497c827 + +Message = 64-byte repeating sample +Tag = 51f0bebf7e3b9d92fc49741779363cfe +``` + +Read the tool's top-of-file comment; the four vectors are exercised against the +signer's internal CMAC routine before any real image is signed. + +--- + +## Part 4: The Hardware and the Flash Map + +### The Pieces + +| Component | Role in this lab | +| ---------------- | ------------------------------------------------------------------- | +| Pico 2 (RP2350) | The target. Two Cortex-M33 cores, 512 KB SRAM, executes code in place from XIP flash. | +| Debug Probe | CMSIS-DAP SWD adapter for flashing, debugging, and UART serial. It owns the SWD port and usually also gives a UART console channel. | +| IR receiver | GPIO5 input; decodes NEC remote commands in the application. | +| 3 LEDs | GPIO16, GPIO17, GPIO18; driven by decoded commands. | +| USB-UART/cable | If the Debug Probe's UART is wired to the Pico's UART0, the bootloader and app log to it. | + +### Why 0x10000000? + +On the RP2350 the first 4 MiB of the address map (from `0x10000000`) is a +**memory-mapped view of external XIP flash** (eXecute In Place). The CPU +fetches instructions straight from that window; the flash controller reads the +bytes transparently through the XIP cache. The boot ROM boots from this flash +window because it is simply memory. + +So "flashing at `0x10000000`" means "making the contents of physical flash +visible at the addresses your program expects." A raw `.bin` carries no address +information; you must always supply the load address out of band (Part 12). + +### The 0x0026a Flash Layout + +``` +Physical XIP flash, as addressable memory ++---------------------------------------------- 0x10000000 +| BOOTLOADER image (0x0026a_bootloader.bin) | +| ~28 KB of code + data | +| boot.c, verify.c, manifest.c, aes_cmac.c, | +| image.c, main.c | ++---------------------------------------------- ~0x10006D00 +| (free padding) | ++---------------------------------------------- 0x1001F000 +| MANIFEST sector (4 KB, exactly one flash | +| sector): magic, version, app address, | +| app size, 16-byte AES-128-CMAC tag | ++---------------------------------------------- 0x10020000 +| APPLICATION image (0x0026a_app.bin) | +| the Week 11 IR remote control app, | +| now with an AES-128-CBC secret banner, | +| linked to run from 0x10020000 | ++---------------------------------------------- 0x10020000 + 0x5498 +| (free padding) | ++---------------------------------------------- 0x10200000 (2 MiB) +``` + +Three things to notice: + +1. The manifest sits in its **own flash sector** (`0x1001F000`), so updating + it never touches the bootloader or the application. +2. The application is **relocated** above both, starting at `0x10020000`. It is + not written at `0x10000000`. +3. The bootloader jump at the end of Part 8 hands control to the app's reset + vector, which lives at `0x1002015C`. + +--- + +## Part 5: The Manifest Format + +Before code runs, the bootloader reads a 4 KB record from flash. The first 28 +bytes matter; the rest is `0xFF` padding (erased flash). + +``` +Declared in bootloader/include/manifest.h +#pragma pack(push, 1) +typedef struct { + uint32_t magic; // 0x424C4657 ("WFLB") + uint32_t version; // 1 + uint32_t app_base; // 0x10020000 + uint32_t app_size; // 0x5498 (21656 bytes) + uint8_t tag[16]; // AES-128-CMAC of the app image +} fw_manifest_t; +#pragma pack(pop) +``` + +Annotated hex dump of the real `manifest.bin` produced in Part 11: + +``` +00000000: 57 46 4c 42 01 00 00 00 00 00 02 10 98 54 00 00 WFLB........T.. + ^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^ + magic version app_base app_size + 0x10020000 0x5498 + +00000010: ef 04 1e c5 92 f0 c5 67 27 da 26 51 d3 73 94 7b .......g'.&Q.s.{ + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + tag (16 bytes): AES-128-CMAC over the application +00000020: ff ff ff ff ... (0xFF padding to 0x1000) +``` + +Note the little-endian layout. `00 00 02 10` reads as `0x10020000`, and +`98 54 00 00` reads as `0x5498`. The tag is the raw 16-byte CMAC output, not a +text string. + +--- + +## Part 6: The Custom Linker Script (Where Your Program Really Lives) + +This is the part that answers a question students ask constantly: *"If I want +to put a second program above the first one, how does the compiler know where +it belongs?"* Answer: **the linker script says so.** Understand this file and +you control a device's entire memory layout; ignore it and relocated firmware +silently corrupts itself. + +### 6.1 What a Linker Script Is + +Compilation produces **object files** with named sections: `.text` (code), +`.rodata` (constants), `.data` (initialized globals), `.bss` (zeroed globals), +and so on. The linker (here, the GNU `arm-none-eabi-ld`) merges those sections +into a single ELF and decides **which virtual addresses each section gets**. +That decision comes from a script, normally `/path/to/lds` or, for the Pico +SDK, a script your CMake passes in. + +A copy of the RP2350 script lives here: + +``` +app/memmap_app.ld <- our modified version, used for the app only +``` + +The SDK's original (do not edit) is inside the Pico SDK: + +``` +~/.pico-sdk/sdk/2.2.0/src/rp2_common/pico_crt0/rp2350/memmap_default.ld +``` + +### 6.2 The MEMORY Command: ORIGIN and LENGTH + +The heart of the script is the `MEMORY` block. It tells the linker which +address ranges are real and how big they are. The SDK's default is: + +``` +/* from memmap_default.ld */ +MEMORY +{ + INCLUDE "pico_flash_region.ld" + RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 512k + SCRATCH_X(rwx) : ORIGIN = 0x20080000, LENGTH = 4k + SCRATCH_Y(rwx) : ORIGIN = 0x20081000, LENGTH = 4k +} +``` + +What each line says: + +| Region | ORIGIN | LENGTH | What lives there | +| ------------ | ----------- | ------ | --------------------------------------------------- | +| FLASH | 0x10000000 | board flash size | `.text`, `.rodata`, and initial values for `.data` (XIP). | +| RAM | 0x20000000 | 512k | `.data`, `.bss`, heap | +| SCRATCH_X | 0x20080000 | 4k | Core 1 stack / time-critical scratch | +| SCRATCH_Y | 0x20081000 | 4k | Core 0 stack (ends at `0x20082000`) | + +`ORIGIN` is the first byte of the region; `LENGTH` is how many bytes it spans. +Anything placed in `FLASH` runs directly from the XIP window; anything placed +in a RAM region is copied there at boot by the `_reset_handler`. + +### 6.3 Where FLASH Actually Comes From: `pico_flash_region.ld` + +Notice that `FLASH` is not declared inline; it is **included**: + +``` +INCLUDE "pico_flash_region.ld" +``` + +The SDK's CMake generates this tiny file for you based on the board's flash +size. Ours (from the build directory) is: + +``` +/* build/pico_flash_region.ld */ +FLASH(rx) : ORIGIN = 0x10000000, LENGTH = (4 * 1024 * 1024) +``` + +So for the default Pico 2 the whole 4 MiB of flash is one region called +`FLASH`, read-only + execute, starting at `0x10000000`. If you build a board +with 2 MiB you would see `LENGTH = (2 * 1024 * 1024)`. + +### 6.4 The Modification: Relocating the Application + +The bootloader owns the start of flash, so the app must not begin at +`0x10000000`. We copy the SDK script and change **one thing in MEMORY**: the +FLASH line. In `app/memmap_app.ld`: + +``` +MEMORY +{ + /* Relocated for the custom bootloader: the application is linked to run + from 0x10020000, leaving the first 128k of flash for the bootloader and + its manifest. */ + FLASH(rx) : ORIGIN = 0x10020000, LENGTH = 1920k + RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 512k + SCRATCH_X(rwx) : ORIGIN = 0x20080000, LENGTH = 4k + SCRATCH_Y(rwx) : ORIGIN = 0x20081000, LENGTH = 4k +} +``` + +Arithmetic behind `1920k`: + +``` +default FLASH region: 0x10000000 + 2048 KiB (2 MiB reserved layout) +reserved for boot/manifest: + bootloader image ~28 KiB, manifest at 0x1001F000..0x10020000 + -> reserve the first 128 KiB (0x20000 bytes) +application window: 0x10020000 + 1920 KiB = 0x10200000 end of 2 MiB +1920 KiB = 2048 KiB - 128 KiB +``` + +Every `> FLASH` and `> RAM AT> FLASH` section in the rest of the script now +places itself inside this smaller, relocated window. The `SECTIONS` body is +otherwise identical to the SDK default. + +### 6.5 Wiring the Script Into the Build (CMake) + +The SDK exposes a CMake function that swaps in our script for one target only: + +``` +# CMakeLists.txt +pico_set_linker_script(0x0026a_app + ${CMAKE_CURRENT_LIST_DIR}/app/memmap_app.ld) +``` + +The bootloader target gets no such call, so it uses the SDK default +(`FLASH` at `0x10000000`) exactly as before. + +### 6.6 Symbols the Script Defines + +A linker script does not only place sections; it also **exports symbol values** +that startup code uses. Grep `memmap_app.ld` for these while you read: + +| Symbol | Meaning | Value for the app | +| ---------------------- | -------------------------------------------------------------- | --------------------------- | +| `__flash_binary_start` | First byte of the image in flash | `0x10020000` | +| `__logical_binary_start`| Same as above in this script | `0x10020000` | +| `__etext` | End of code (start of `.data` initial values in flash) | near `0x10026000` | +| `__data_start__` | Loaded `.data` base in RAM | `0x20000000` | +| `__bss_start__`/`__bss_end__` | Zero-initialized globals in RAM | inside `0x20000000` region | +| `__StackLimit` | Heap cap (historically the "stack limit") | `0x20080000` | +| `__StackTop` | Top of core 0 stack (end of SCRATCH_Y) | `0x20082000` | +| `__HeapLimit` | End of RAM = `ORIGIN(RAM) + LENGTH(RAM)` | `0x20080000` | + +The crucial practical consequence: **the first word of the app's vector table +must be `__StackTop` = `0x20082000`.** +Check it: + +``` +$ xxd -l 16 0x0026a_app.bin +00000000: 0020 0820 5d01 0210 1b01 0210 1d01 0210 . . ]........... +``` + +- bytes 0-3 `00 20 08 20` = little-endian `0x20082000` = initial SP +- bytes 4-7 `5d 01 02 10` = little-endian `0x1002015d` = reset vector (the + `_reset_handler` symbol, Thumb-encoded with bit 0 set) + +That `0x1002015d` would be impossible to obtain without the relocated MEMORY +block. This is the tangible proof that the linker script did its job. + +### 6.7 ENTRY(_entry_point) Versus the Reset Vector + +At the top of the script: + +``` +ENTRY(_entry_point) +``` + +This names the ELF entry point used by debuggers. In the SDK, `_entry_point` +is a tiny stub in `crt0.S` that routes a debugger-launched session back +through the boot ROM (so XIP setup happens as on a cold boot). It is +**not** the same as the reset vector in the vector table. + +The bootloader does not use `_entry_point` at all. It jumps straight at the +app's **reset vector** (`vector[1]`), which is `_reset_handler`, the function +that actually initializes `.data`, zeros `.bss`, and calls `main`. Part 8 +shows that branch instruction. This distinction matters when you are poking a +relocated image with a debugger: breaking on `_entry_point` and breaking on +`_reset_handler` are different sessions. + +### 6.8 Inspecting the Result With readelf and nm + +After building (Part 10), look at what the linker produced. The section table: + +``` +$ ~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-readelf -S build/0x0026a_app.elf + [ 0] null + [ 1] .flash_begin PROGBITS 10020000 00001000 000000 00 WA 0 0 1 + [ 2] .text PROGBITS 10020100 00001100 005e34 00 AX 0 0 16 + ... + [ 5] .rodata PROGBITS 10025f40 00006f40 000235 00 A 0 0 8 + ... +``` + +Note that every flash section is stamped `1002xxxx`, never `1000xxxx`. The +program headers (use `readelf -l`) likewise show `LOAD` segments whose vaddr +sits in the `0x10020000` window. `nm` shows symbol addresses: + +``` +$ ~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-nm -n build/0x0026a_app.elf +... +1002015c T _reset_handler +10020234 T main +1002027c T ir_init +... +``` + +### 6.9 Why the Application Has No .uf2 + +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. + +--- + +## Part 7: The Bootloader Source, Module by Module + +The bootloader applies the standard we use for every module in this course: +**`main.c` contains only the sequence; each job has its own `.c` + `.h` pair.** + +``` +bootloader/ + include/ + flash_layout.h addresses of the three flash partitions + manifest.h fw_manifest_t type + read/validate API + boot.h console, halt, launch API + verify.h firmware authenticity check + aes_cmac.h mbedtls front-end + constant-time compare + image.h low-level jump to the app + src/ + main.c ONLY the 5-step boot sequence + boot.c banner console, fatal-haunt loop, handoff + manifest.c flash-read + magic/version validation + verify.c the static key + CMAC recompute + tag compare + aes_cmac.c computes the tag with mbedtls AES-128-CMAC + image.c VTOR/MSP rebase + branch to reset vector +``` + +### 7.1 `main.c`: Only the Sequence + +The whole of `main.c`: + +```c +int main(void) { + fw_manifest_t manifest; + boot_init(); + manifest_read(&manifest); + if (!manifest_is_valid(&manifest)) boot_halt("manifest invalid"); + if (!verify_manifest(&manifest)) boot_halt("firmware signature invalid"); + boot_launch(&manifest); +} +``` + +Read it as a check-list, one step per line: + +1. `boot_init()` - stdio up, 2 s pause, banner print +2. `manifest_read` - copy the 28 useful bytes out of the `0x1001F000` sector +3. `manifest_is_valid` - magic `WFLB` and version == 1? +4. `verify_manifest` - recompute CMAC over the app, compare to stored tag +5. `boot_launch` - print success, jump to the app reset vector + +There is **no** stdio, no key, no crypto in this file. Everything below is a +different module. + +### 7.2 `verify.c`: The Secret and the Verdict + +```c +/** @brief AES-128 key that authenticates the application image. */ +static const uint8_t fw_key[FW_TAG_SIZE] = { + 0x49, 0x72, 0x6F, 0x6E, 0x4E, 0x65, 0x74, 0x42, + 0x6F, 0x6F, 0x74, 0x4B, 0x65, 0x79, 0x21, 0x21 +}; + +bool verify_manifest(const fw_manifest_t *manifest) { + const uint8_t *base = (const uint8_t *)manifest->app_base; + uint8_t computed[FW_TAG_SIZE]; + int rc = aes128_cmac(fw_key, base, manifest->app_size, computed); + if (rc != 0) return false; + return cmac_tags_equal(computed, manifest->tag); +} +``` + +What is important here: + +- The key is 16 bytes `"IronNetBootKey!!"`. It is `static const`, so the + compiler puts it in `.rodata`, right in the binary, at a predictable + address (you will extract it in Ghidra in Part 15). +- The CMAC is computed over **the raw bytes the processor will execute**, + starting at `manifest->app_base` for `app_size` bytes. That is exactly the + `0x0026a_app.bin` content (21656 bytes, `0x5498`). +- The comparison is deliberately a full 16-byte compare with no shortcut, so + the runtime does not leak *which* byte mismatched (timing side-channels). + +### 7.3 `boot.c`: Console and Handoff + +```c +void boot_init(void) { + stdio_init_all(); + sleep_ms(2000); + printf("\n=== 0x0026a secure bootloader ===\n"); +} + +void boot_halt(const char *reason) { + printf("BOOT FAILED: %s\n", reason); + while (true) { + sleep_ms(500); + } +} + +void boot_launch(const fw_manifest_t *manifest) { + printf("signature valid, entering app at 0x%08lx\n", + (unsigned long)manifest->app_base); + image_start(manifest->app_base); +} +``` + +A 2 000 ms delay before the banner gives a human time to open a serial +terminal; the blink of that pause is also a simple "alive" signal. On failure, +`boot_halt` loops forever with a 500 ms heart-beat so you can see the red LED +blinking and the exact reason on the console. + +### 7.4 `image.c`: The Actual Jump + +```c +static void halt_irq_and_rebase(uint32_t base) { + __asm volatile ("cpsid i" ::: "memory"); + *(volatile uint32_t *)0xE000ED08U = base; +} + +void image_start(uint32_t vector_base) { + uint32_t *vectors = (uint32_t *)vector_base; + image_entry_t entry = (image_entry_t)vectors[1]; + halt_irq_and_rebase(vector_base); + __asm volatile ("msr msp, %0" :: "r" (vectors[0]) : "memory"); + __asm volatile ("dsb\n\tisb" ::: "memory"); + entry(); +} +``` + +Step by step: + +1. Read the app's vector table at `vector_base` (`0x10020000`). +2. `entry = vectors[1]` - the reset vector (`0x1002015d`), bit 0 set for + Thumb mode. Branching to a Thumb-encoded address is how you switch modes. +3. `cpsid i` - mask interrupts so nothing fires mid-handoff. +4. Write `0xE000ED08` (the **VTOR** register) with the new vector table base. + From here on, exceptions use the app's vectors. +5. `msr msp` - load the app's initial stack pointer from `vectors[0]` + (`0x20082000`). +6. `dsb`/`isb` - force the memory and instruction pipelines to catch up. +7. `entry()` - call the reset handler. It never returns to the bootloader. + +The assembly, as it appears in the ELF (this is exactly what you will +disassemble in GDB and Ghidra): + +``` +10000394 : + 10000394: 6843 ldr r3, [r0, #4] ; entry = vectors[1] + 10000396: b672 cpsid i ; mask IRQs + 10000398: mov.w r2, #0xe000e000 ; SCB base + 1000039c: str.w r0, [r2, #3336] ; VTOR = r0 (0xE000ED08) + 100003a0: ldr r2, [r0, #0] ; sp = vectors[0] + 100003a2: msr MSP, r2 + 100003a6: dsb sy + 100003aa: isb sy + 100003ae: bx r3 ; branch to reset handler +``` + +When you set a breakpoint on `image_start` and single-step, you will see the +PC jump from `100003ae` to `1002015d` and disappear into application startup. + +--- + +## Part 8: Using AES in the Application (a Fully Worked Example) + +The application is the Week 11 IR remote firmware, but with one addition: a +module (`secret.c`) that decrypts a banner at runtime using the **Mbed TLS AES +that ships inside the Pico SDK**. This part teaches you the SDK's real AES API +on real code, and it plants the exact artifact you will hunt for in the +reverse-engineering parts. + +### 8.1 The API You Need (all from mbedtls, all shipped in the SDK) + +Four calls. That is the entire surface we touch: + +| Call | Purpose | +| --------------------------------------- | ---------------------------------------------- | +| `mbedtls_aes_init(ctx)` | Give the context a clean base state. | +| `mbedtls_aes_setkey_dec(ctx, key, 128)` | Expand the 16-byte key for decryption. | +| `mbedtls_aes_crypt_cbc(ctx, mode, len, iv, src, dst)` | Decrypt one or more whole blocks. | +| `mbedtls_aes_free(ctx)` | Clean up (releases nothing for a stack context). | + +`mbedtls_aes_crypt_cbc` is the workhorse. Its important contract: + +- `len` must be a multiple of 16 (a whole number of AES blocks). +- The IV is **read and updated in place**; you must give it a mutable copy. +- `src` and `dst` may be the same pointer - the operation is **in-place**. + That is how `secret_decrypt` reuses one 33-byte buffer for ciphertext, then + plaintext. + +### 8.2 The Secret Banner Link: Host Side Production + +The plaintext banner is exactly 32 bytes (two AES blocks), so no padding is +needed. It was encrypted on the host with OpenSSL using the same key and IV: + +``` +$ printf '%s' 'SECRET: IR LINK SECURED ONLINE ' \ + | openssl enc -aes-128-cbc \ + -K 49726f6e4e6574426f6f744b65792121 \ + -iv 41707053656372657449563132333435 \ + -nopad | xxd -p +baec4a1925861bb92dfd9f13f79b3e80cf12b698db9165be8fd7c3477bb2e76e +``` + +The hex strings are: + +``` +KEY = 49 72 6F 6E 4E 65 74 42 6F 6F 74 4B 65 79 21 21 = "IronNetBootKey!!" +IV = 41 70 70 53 65 63 72 65 74 49 56 31 32 33 34 35 = "AppSecretIV12345" +CT = ba ec 4a 19 25 86 1b b9 2d fd 9f 13 f7 9b 3e 80 + cf 12 b6 98 db 91 65 be 8f d7 c3 47 7b b2 e7 6e +PT = "SECRET: IR LINK SECURED ONLINE " (32 bytes, two spaces padded) +``` + +Notice: the ciphertext bytes in the disassembly of `secret_print_banner` +(Part 16) are **exactly** `baec4a19...76e6`. You will re-discover that the +reverse engineers' job is rarely to break AES (you cannot); it is to find +where the key, IV, and ciphertext are stored and feed them to a decryptor. +Here the key is 70 bytes away from the ciphertext. + +### 8.3 The Application-Side Module (`secret.c`) + +```c +static int secret_cbc(mbedtls_aes_context *aes, uint8_t *iv, uint8_t *buf) { + int rc = mbedtls_aes_crypt_cbc(aes, MBEDTLS_AES_DECRYPT, APP_BANNER_LEN, + iv, buf, buf); + return rc; +} + +static int secret_decrypt(uint8_t *buf) { + mbedtls_aes_context aes; + uint8_t iv[APP_AES_BLOCK]; + memcpy(iv, banner_iv, APP_AES_BLOCK); + mbedtls_aes_init(&aes); + mbedtls_aes_setkey_dec(&aes, banner_key, 128); + int rc = secret_cbc(&aes, iv, buf); + mbedtls_aes_free(&aes); + return rc; +} + +int secret_print_banner(void) { + uint8_t buf[APP_BANNER_LEN + 1]; + memcpy(buf, banner_ct, APP_BANNER_LEN); + buf[APP_BANNER_LEN] = '\0'; + int rc = secret_decrypt(buf); + if (rc != 0) return rc; + printf("app banner: %s\n", (char *)buf); + return 0; +} +``` + +Walk through it: + +1. `memcpy(buf, banner_ct, 32)` - pull the two ciphertext blocks into a working + buffer (with a spare byte for the terminating `'\0'`). +2. `banner_iv` is `static const`, so it cannot be safely mutated by the CBC + routine; copy it to a local array first. +3. Init + expand the key ("setkey_dec" stores the pre-computed decryption key + schedule once, so `crypt_cbc` is fast). +4. `crypt_cbc` writes the plaintext back into `buf`: `src == dst`. +5. Print. Release. + +If the SDK's mbedtls were misconfigured (say, you forgot to enable +`MBEDTLS_AES_C` in `common/include/mbedtls_config.h`), `crypt_cbc` returns a +negative error instead of decrypting, and the module refuses to print garbage. + +### 8.4 CBC, In Pictures + +CBC decryption undoes each block with the *previous* ciphertext block: + +``` ++------+ +------+ +------+ +| C1 | | C2 | | C3 | ciphertext blocks ++------+ +------+ +------+ + | | | + v v v + AES-128 AES-128 AES-128 (key schedule from setkey_dec) + | | | + | | | + v v v + +--[ X XOR ]--+ +--[ X XOR ]--+ + | IV | | C1 | | C2 | XOR with IV/C[i-1] + +-------------+ +-------------+ +-------------+ + | | | + v v v ++------+ +------+ +------+ +| P1 | | P2 | | P3 | plaintext ++------+ +------+ +------+ +``` + +That is why the IV is the "previous block" for block 1. In-place operation is +safe because C1 is no longer needed once P1 is produced ... except block two +needs C1, so the ciphertext *before* it was overwritten must still be intact. +mbedtls handles this correctly internally; you only owe it an untouched IV +copy. + +### 8.5 How the Application Is Structured (modules, not a mega-file) + +This week the app got the same treatment as the bootloader: `app_main.c` is +only entry + the poll loop. Everything else lives in a module: + +``` +app/ + include/ + ir.h NEC receiver API (ir_init, ir_getkey) + led.h LED controller type + API (simple_led_ctrl_t, led_default, + led_init, led_handle_ir) + secret.h AES banner API (secret_print_banner) + src/ + app_main.c ONLY stdio, banner, led_init, ir_init, then the poll loop + ir.c NEC timing decoder + led.c 3-LED controller (mapping, blink, hold-on) + secret.c the AES-128-CBC banner module +``` + +`app_main.c` in full: + +```c +static void ir_poll_loop(simple_led_ctrl_t *leds) { + while (true) { + int key = ir_getkey(); + if (key >= 0) led_handle_ir(leds, key); + else sleep_ms(1); + } +} + +int main(void) { + stdio_init_all(); + secret_print_banner(); + simple_led_ctrl_t leds = led_default(); + led_init(&leds); + ir_init(IR_PIN); + printf("IR receiver on GPIO %d ready\n", IR_PIN); + ir_poll_loop(&leds); +} +``` + +The `simple_led_ctrl_t` type carries a Doxygen block in `led.h`; notice the +struct fields (`led1_pin`...`led3_state`) are visible in `ir_getkey`'s +callers but the types stay in the header, exactly the WEEK 11 lesson about +structs being flattened by the compiler (see the `led_handle_ir` disassembly +in Part 16 for the flattened, inlined version). + +--- + +## Part 9: Building Everything + +### Step 1: Configure the CMake build + +From the project directory: + +``` +$ cd 0x0026a_custom-bootloader + +$ cmake -B build -G Ninja \ + -DPICO_BOARD=pico2 \ + -DPICO_PLATFORM=rp2350 \ + -DCMAKE_BUILD_TYPE=Release \ + -DPICO_SDK_PATH="$HOME/.pico-sdk/sdk/2.2.0" \ + -DPICO_TOOLCHAIN_PATH="$HOME/.pico-sdk/toolchain/14_2_Rel1" +``` + +### Step 2: Build both images + +``` +$ cmake --build build +``` + +You should see two linked executables, no warnings: + +``` +[6/7] Linking CXX executable 0x0026a_app.elf +[7/7] Linking CXX executable 0x0026a_bootloader.elf +``` + +### Step 3: Check the artifacts + +``` +$ ls -l build/0x0026a_bootloader.bin build/0x0026a_app.bin +-rwxr-xr-x 27772 0x0026a_bootloader.bin +-rwxr-xr-x 21656 0x0026a_app.bin +``` + +The bootloader includes the second-stage loader and binary-info, so it is +larger; the app is 21656 bytes (`0x5498`), which is the number baked into the +manifest. + +### Step 4: Confirm the app really is relocated + +``` +$ ~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-nm -n build/0x0026a_app.elf | grep -E ' _reset_handler| main$| ir_getkey| secret_print_banner' +1002015c T _reset_handler +10020234 T main +100202a4 T ir_getkey +10020568 T secret_print_banner +``` + +Every address is `0x1002xxxx`. If you see `0x1000015c` for `_reset_handler`, +the linker script was not applied and the bootloader would jump the wrong way. + +--- + +## Part 10: Signing the Image on the Host + +### Step 5: Compute the tag and write the manifest + +``` +$ python3 tools/sign_firmware.py build/0x0026a_app.bin --out build/manifest.bin +image : build/0x0026a_app.bin (21656 bytes) +app_base : 0x10020000 +cmac tag : ef041ec592f0c56727da2651d373947b +manifest : build/manifest.bin (4096 bytes) +``` + +### Step 6: Verify the manifest against the image + +``` +$ python3 tools/sign_firmware.py 0x0026a_app.bin --verify build/manifest.bin +computed : ef041ec592f0c56727da2651d373947b +result : MATCH +``` + +MATCH means the exact bytes in `0x0026a_app.bin` produce exactly the tag the +bootloader will read from flash. Signing is not magic: it is one CMAC +computation over the bytes, plus a 36-byte header written into a 4 KB sector. + +### Step 7: Read the manifest back byte by byte + +``` +$ xxd build/manifest.bin | head -3 +00000000: 5746 4c42 0100 0000 0000 0210 9854 0000 WFLB........T.. +00000010: ef04 1ec5 92f0 c567 27da 2651 d373 947b .......g'.&Q.s.{ +00000020: ffff ffff ffff ffff ffff ffff ffff ffff ................ +``` + +Cross-reference with Part 5: magic, version 1, base `0x10020000`, size +`0x5498`, then the 16-byte tag, then `0xFF` padding. + +--- + +## Part 11: Flashing the Three Images + +Three images, three addresses. The bootloader first (it owns the system), then +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. + +### 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. + +**Option 2 - OpenOCD:** + +``` +$ "$HOME/.pico-sdk/openocd/0.12.0+dev/openocd" \ + -s "$HOME/.pico-sdk/openocd/0.12.0+dev/scripts" \ + -f interface/cmsis-dap.cfg -f target/rp2350.cfg \ + -c "adapter speed 5000" \ + -c "program 0x0026a_bootloader.bin 0x10000000 verify reset exit" +``` + +The flags, in the repo's own words: + +| Flag | Meaning | +| --------------- | ------------------------------------------------------------- | +| `cmsis-dap.cfg` | The Debug Probe is a CMSIS-DAP v1 adapter | +| `rp2350.cfg` | RP2350 target: dual Cortex-M33 with a B0-style DAP | +| `adapter speed` | SWD clock in kHz; 5000 is a conservative write-path default | +| `program` | Write + `verify` (read-back compare) + `reset` + `exit` | + +### B. The manifest (always its own sector) + +``` +$ "$HOME/.pico-sdk/openocd/0.12.0+dev/openocd" \ + -s "$HOME/.pico-sdk/openocd/0.12.0+dev/scripts" \ + -f interface/cmsis-dap.cfg -f target/rp2350.cfg \ + -c "adapter speed 5000" \ + -c "program manifest.bin 0x1001F000 verify reset exit" +``` + +### C. The application (relocated target) + +``` +$ "$HOME/.pico-sdk/openocd/0.12.0+dev/openocd" \ + -s "$HOME/.pico-sdk/openocd/0.12.0+dev/scripts" \ + -f interface/cmsis-dap.cfg -f target/rp2350.cfg \ + -c "adapter speed 5000" \ + -c "program 0x0026a_app.bin 0x10020000 verify reset exit" +``` + +Each `program` invocation ends with `verify reset exit`, so OpenOCD reads the +bytes back and aborts loudly on a mismatch. `verify` here is straight +flash read-back - a completely different check from the CMAC the bootloader +does at boot. + +While you are at it, note that the repo's `flash.sh` helper always programs at +`0x10000000` - convenient for the bootloader, wrong for the manifest and app. +The three explicit commands above are the tutorial forms. + +--- + +## Part 12: Running It (First Boot, Serial Console) + +### Step 8: Open a serial console + +Open a terminal on the Debug Probe's UART channel (or the board's USB-UART +bridge if you wired one) at 115200 baud: + +``` +$ screen /dev/cu.usbmodem311402 115200 +``` + +Replace the device name with whatever `ls /dev/cu.usb*` shows. + +### Step 9: Reset the board and watch the bootlog + +Press the RESET button (or let OpenOCD's `reset` do it). After roughly two +seconds you see the bootloader banner, then the app banner: + +``` +=== 0x0026a secure bootloader === +signature valid, entering app at 0x10020000 +app banner: SECRET: IR LINK SECURED ONLINE +IR receiver on GPIO 5 ready +``` + +The three lines tell you the whole story: the bootloader ran, it verified the +image against the manifest (Part 10), it jumped to `0x10020000`, and inside +the app `secret_print_banner()` decrypted the banner with the SDK AES and +printed it (Part 8). Press keys on the remote and the `NEC command:` lines +appear. + +What you just watched, in one trace (the exact functions and where they live): + +| Line on screen | Function that printed it | Address | +| ------------------------------------------- | ------------------------ | ------------ | +| `=== 0x0026a secure bootloader ===` | `boot_init` | `0x10000278` | +| `signature valid, entering app at ...` | `boot_launch` | `0x100002ac` | +| `app banner: SECRET...` | `secret_print_banner` | `0x10020568` | +| `IR receiver on GPIO 5 ready` | `main` (app) | `0x10020234` | + +--- + +## Part 13: The Tamper Test (Breaking the Chain) + +Now do the attacker's favorite move: change one byte, keep everything else, +reflash, and see what the bootloader does. + +### Step 10: Flip a byte in the app image (do NOT re-sign) + +``` +$ cp 0x0026a_app.bin rogue_app.bin +$ printf 'X' | dd of=rogue_app.bin bs=1 seek=0 count=1 conv=notrunc +``` + +(The byte at offset 0 is the low byte of the initial SP; any change breaks the +CMAC, so the location is irrelevant for this test.) + +### Step 11: Flash the rogue image over the good one + +Re-run the app `program` command from Part 11 but with `rogue_app.bin`. Do not +touch the manifest - the tag inside it still corresponds to the *original* +image. + +### Step 12: Reset and observe the rejection + +``` +=== 0x0026a secure bootloader === +BOOT FAILED: firmware signature invalid +``` + +The second line is missing. The app never ran. The blue/yellow LED heartbeat +on the board keeps blinking (500 ms), which is `boot_halt()`'s idle loop. No +matter how many times you re-flash a modified image without a matching tag, +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. + +Stop and think about the security statement this demonstrated: + +- **Patch a byte, no re-sign -> reject.** The device cannot be modified cheaply. +- **Dump flash, read the key, re-sign -> accept.** Because the key lives in the + binary (Part 7.2), a full flash dump defeats this scheme. Part 19 fixes that + in a real product. + +--- + +## Part 14: Reverse Engineering the Bootloader with GDB (Live) + +Live debugging on the Pico 2 uses the repo's two scripts: `debug-server.sh` +owns the Debug Probe as a GDB server, and `pico-gdb-memory.gdb` confines GDB +memory reads to valid flash/RAM so bogus register previews cannot fault SWD. + +### Step 14: Start the GDB server (Terminal 1) + +``` +$ ./debug-server.sh +``` + +The script resets the target and leaves it *running* (`reset run`), exposing +`127.0.0.1:3333`. + +### Step 15: Start GDB on the bootloader (Terminal 2) + +``` +$ ~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb build/0x0026a_bootloader.elf +``` + +Connect and confine memory access: + +``` +(gdb) target extended-remote :3333 +(gdb) source ../../pico-gdb-memory.gdb +``` + +``` +delete mem +mem 0x10000000 0x10400000 ro +mem 0x20000000 0x20082000 rw +set mem inaccessible-by-default on +info mem +``` + +### Step 16: Stop the running target cleanly + +``` +(gdb) monitor halt +(gdb) monitor reg pc +``` + +You will land somewhere in the bootloader or app depending on how far the +resets progressed - either is fine. + +### Step 17: Disassemble `main` and read the boot sequence + +``` +(gdb) disassemble 0x10000234,+80 +``` + +You should see the miniature version of Part 7.1: + +``` +0x10000234
: push {lr} +0x10000238 : bl 0x10000278 +0x1000023e : bl 0x100002c8 +0x10000244 : bl 0x100002fc +0x1000024c : bl 0x100003b0 +0x10000254 : bl 0x100002ac +``` + +Note `main` does almost nothing itself - it is a sequence of calls, exactly +the design we built. + +### Step 18: Stop inside the crypto (the payoff breakpoint) + +The interesting code is `verify_manifest` at `0x100003b0` and the mbedtls +front-end `aes128_cmac` at `0x1000031c`. Re-arm a hardware breakpoint and +reset: + +``` +(gdb) hbreak *0x100003b0 +(gdb) monitor reset halt +(gdb) continue +``` + +After 2 s (the startup pause) the core stops at the top of `verify_manifest`, +with `r0` pointing at the manifest copy on the stack. Step a little and watch +the argument registers the mbedtls call receives: + +``` +(gdb) disassemble 0x100003b0,+50 +(gdb) x/4bx 0x10006064 # low nibbles of the key? no: dump the key +(gdb) x/16bx 0x10006064 # the 16-byte fw_key from .rodata +0x10006064: 49 72 6f 6e 4e 65 74 42 6f 6f 74 4b 65 79 21 21 +``` + +`0x49 0x72 0x6F 0x6E ...` is `"IronNetBootKey!!"`. You just pulled the boot +key out of live flash with three GDB commands. (Refresh: the key's virtual +address is `0x10006064`; `nm` reports it as `10006064 r fw_key`.) + +### Step 19: Watch the verdict + +Let it run to the compare: + +``` +(gdb) hbreak *0x100003d0 # the bl cmac_tags_equal +(gdb) continue +``` + +`cmac_tags_equal` compares the freshly computed tag (on the stack) against the +16 bytes at offset +16 of the manifest (which came from flash). Step over it +(`next`) and check the return: + +``` +(gdb) next +(gdb) info registers r0 +``` + +`r0 == 1` means the tags matched and `boot_launch` is next. + +### Step 20: Single-step the application jump + +Set a breakpoint at the tail of `image_start` and keep going: + +``` +(gdb) break *0x100003ae +(gdb) continue +``` + +At `0x100003ae` the core is one `bx r3` away from the app. Inspect what it is +about to execute: + +``` +(gdb) info registers r3 +r3 0x1002015d +(gdb) x/4bx 0x10020000 # the app's vector table +0x10020000: 00 20 08 20 5d 01 02 10 # SP=0x20082000, reset=0x1002015d +``` + +`r3` and `vectors[1]` agree. Step into the jump: + +``` +(gdb) stepi +``` + +Watch the PC: it is now `0x1002015c` (the unencoded reset-vector address of +`_reset_handler`) and the stack pointer is `0x20082000`. The bootloader is +finished; the application startup code owns the machine. + +### Step 21: Re-verify from the application's side + +Quit and relaunch GDB against the app ELF (the core is already running it, so +no reset needed - reattach so symbols load): + +``` +(gdb) file build/0x0026a_app.elf +(gdb) target extended-remote :3333 +(gdb) hbreak *0x10020234 # app main +(gdb) monitor reset halt +(gdb) continue +``` + +When you hit `main`, the banner has already been decrypted and printed (it +runs before `main`). To watch the AES itself, break at +`secret_print_banner` instead: + +``` +(gdb) hbreak *0x10020568 +(gdb) monitor reset halt +(gdb) continue +``` + +Then single-step the four mbedtls calls from Part 8.1. At +`0x10020598` you can read the key argument: + +``` +(gdb) disassemble 0x10020568,+110 +(gdb) x/16bx 0x10024e6c # banner_key in .rodata +``` + +`0x24e6c` is the offset of the key inside the app image (Part 16/17 confirms +by Ghidra). You are reading the same constant three different ways: C source, +GDB, and now Ghidra. + +--- + +## Part 15: Reverse Engineering the Bootloader in Ghidra + +### Step 22: Import the ELF (not the raw .bin) + +Ghidra will happily import either, but the **ELF** brings symbols, section +names, and types. For a stripped factory image you would import the raw `.bin` +as a "Raw Binary" at base `0x10000000`; for this lab, use the ELF and check +the green "found symbols" messages after auto-analysis. + +1. **File > New Project** (non-shared), +2. **File > Import File**, select `build/0x0026a_bootloader.elf`, +3. Language: confirm `ARM LE:LE32:v8M` / Cortex-M33 view, +4. **Analyze** (auto-analysis defaults are fine). + +### Step 23: Find the interesting functions + +Open **Symbol Tree > Functions**: + +| Function | Address | What you will see | +| -------------------- | ------------ | -------------------------------------------------- | +| `main` | `0x10000234` | Five calls, no logic of its own | +| `boot_init` | `0x10000278` | `stdio_init_all`, a wait, a puts | +| `verify_manifest` | `0x100003b0` | the 16-byte key push + `aes128_cmac` + compare | +| `aes128_cmac` | `0x1000031c` | `mbedtls_cipher_info_from_type` + `mbedtls_cipher_cmac` | +| `image_start` | `0x10000394` | the VTOR/MSP/bx block from Part 7.4 | + +Double-click `verify_manifest`. Ghidra's decompiler shows something close to +our C - including the constant table it pulled from `.rodata`. Open the +**Data** panel and navigate to `0x10006064`: Ghidra lists a 16-byte array. It +is the key. Rename it by selecting and pressing `L` (e.g. `fw_key`), then +annotate `main`'s call graph with the whole boot sequence. + +### Step 24: Recover the "spec" from the image + +- List exports/imports: the binary imports `cdns_usb_table`, stdio routines, + and - from `.dynamic` or relocation - nothing surprising for an SDK binary. +- Note `binary_info` header: the SDK stores program_name + (`0x0026a_bootloader`) and build date inside the image at `0x10000100`; this + is exactly what `picotool info` prints and a useful fingerprint for hunting + unknown binaries in the wild. + +--- + +## Part 16: Reverse Engineering the Application in Ghidra (Find the AES) + +The app is the richer target. Repeating Part 15 step 22 with +`build/0x0026a_app.elf` gives you the full symbol table, because we shipped +the ELF. On a real malicious/hardened build you would import +`0x0026a_app.bin` at `0x10020000` (base from `manifest.bin`) and rely on +strings. + +### Step 25: The easy tour (symbols) + +| Symbol | Address | Observations | +| ---------------------- | ------------ | --------------------------------------------------- | +| `_reset_handler` | `0x1002015c` | first code; assumes SP/VTOR set (our bootloader does) | +| `main` | `0x10020234` | the thin orchestrator from Part 8.5 | +| `secret_print_banner` | `0x10020568` | four mbedtls calls; the AES gate | +| `led_handle_ir` | `0x100204d8` | flattened struct logic (see below) | + +Open `main`. Because everything was inlined and optimized, Ghidra shows a +clean calling skeleton - the same one GDB showed in Part 14 Step 21. + +### Step 26: The "struct is a lie" lesson, revisited + +Open `led_handle_ir` at `0x100204d8`. Ghidra's decompiler will *not* show you +`simple_led_ctrl_t` fields by name (the type was flattened at compile time, +WEEK 11 Part 10). Instead you see direct byte loads off the pointer and the +constant compares `0xc`, `0x18`, `0x5e` - the NEC commands. The three +`mcrr`/`strb` groups are the `gpio_put`/`gpio_set_dir` calls inlined from the +LED module. Same behavior, zero struct abstraction, single program counter. + +### Step 27: Find the four AES calls + +Open `secret_print_banner`. The decompiler names the imported mbedtls calls +directly: + +``` +mbedtls_aes_init(ctx) +mbedtls_aes_setkey_dec(ctx, key, 128) +mbedtls_aes_crypt_cbc(ctx, MBEDTLS_AES_DECRYPT, 32, iv, ct_buf, ct_buf) +mbedtls_aes_free(ctx) +``` + +The red referenced addresses are: + +| Data | Address (VA) | Bytes | ASCII | +| ---------------- | ------------ | ------------------------------- | ------------ | +| banner ciphertext| `0x10024d78` | `ba ec 4a 19 ... 7b b2 e7 6e` | (binary) | +| banner IV | `0x10024d9c` | `41 70 70 53 65 63 72 65 74 49 56 31 32 33 34 35` | `AppSecretIV12345` | +| `app banner: %s` | `0x10024d68` | `61 70 70 20 62 61 6e 6e 65 72 3a 20 25 73 0a` | `app banner: %s\n` | +| banner key | `0x10024e6c` | `49 72 6f 6e 4e 65 74 42 6f 6f 74 4b 65 79 21 21` | `IronNetBootKey!!` | + +### Step 28: Decrypt it with what you found + +Ghidra hands you plain values; OpenSSL decrypts for you - you do not need to +recover the source: + +``` +$ printf 'baec4a1925861bb92dfd9f13f79b3e80cf12b698db9165be8fd7c3477bb2e76e' \ + | xxd -r -p \ + | openssl enc -d -aes-128-cbc \ + -K 49726f6e4e6574426f6f744b65792121 \ + -iv 41707053656372657449563132333435 -nopad +SECRET: IR LINK SECURED ONLINE +``` + +Because the key is 70 bytes from its ciphertext in the same ELF, "breaking" +the app took: import, click, copy three hex constants, run one command. The +whole exercise is about **finding key material**, never about cracking AES. + +### Step 29: Now repeat it on the stripped .bin (harder, real-world) + +Delete the app from the project, import `0x0026a_app.bin` with **Language = +ARM LE v8M** and **Image base = 0x10020000**. No symbols. Now your anchors +are: + +- The string `app banner: %s` (String search, `Ctrl+Shift+S`), which xrefs to + the second half of `secret_print_banner`. +- The 16 bytes `AppSecretIV12345` nearby in `.rodata`. +- The constant `0x48564d411f...` = `AppSecretIV12345` pattern as 4 little + endian words (this is your Ghidra jump-start trick: search for the hex of an + ASCII IV). + +Renaming data (`L`) and entering type views as you go will reconstruct the +module structure from the stripped blob exactly as if a debugger had been +kind. + +--- + +## Part 17: A Bootloader You Can Read (Reference Address Map) + +Everything you should have collected so far, in one table. Builds are +deterministic for the checked-in sources, so your numbers should match. + +### Bootloader (0x0026a_bootloader.elf) + +| Role | Symbol | Address | +| ----------------------- | ------------------- | ------------ | +| Boot sequence | `main` | `0x10000234` | +| Console + 2 s pause | `boot_init` | `0x10000278` | +| Failure idle loop | `boot_halt` | `0x10000294` | +| Success handoff | `boot_launch` | `0x100002ac` | +| Flash manifest read | `manifest_read` | `0x100002c8` | +| Magic/version check | `manifest_is_valid` | `0x100002fc` | +| mbedtls CMAC front-end | `aes128_cmac` | `0x1000031c` | +| Constant-time compare | `cmac_tags_equal` | `0x10000348` | +| VTOR/MSP/bx jump | `image_start` | `0x10000394` | +| CMAC verdict | `verify_manifest` | `0x100003b0` | +| Boot key (rodata) | `fw_key` | `0x10006064` | + +### Application (0x0026a_app.elf) + +| Role | Symbol | Address | +| ----------------------- | --------------------- | ------------ | +| App reset vector | `_reset_handler` | `0x1002015c` | +| App entry point | `main` | `0x10020234` | +| NEC decoder init | `ir_init` | `0x1002027c` | +| NEC blocking decode | `ir_getkey` | `0x100202a4` | +| LED default config | `led_default` | `0x10020498` | +| LED GPIO setup | `led_init` | `0x100204a8` | +| IR -> LED dispatch | `led_handle_ir` | `0x100204d8` | +| AES banner decrypt | `secret_print_banner` | `0x10020568` | +| String `app banner: %s` | (rodata) | `0x10024d68` | +| Banner ciphertext | (rodata) | `0x10024d78` | +| Banner IV | (rodata) | `0x10024d9c` | +| Banner key | (rodata) | `0x10024e6c` | +| Initial SP (vector[0]) | (data) | `0x20082000` | + +--- + +## Part 18: Summary and Review + +### What You Built + +``` ++-----+ +-----+ +---------+ +| ROM | ------> | BL | -----> | APP | +| | boots | 0x1 | verif. | 0x10020 | +| | bootl. | 000 | aes128 | 000 | +| | | 000 | cmac | | ++-----+ +-----+ +---------+ + | + +--reads-+ manifest @ 0x1001F000 (tag) + | | + +--reads-+ app @ 0x10020000 (image bytes) +``` + +Enforcement point: `verify_manifest` recomputes +`AES_CMAC(fw_key, app_base, app_size)` and compares to the stored tag. On +match, `image_start` sets VTOR, loads SP, and branches to the app reset +vector. On mismatch, `boot_halt` reports and idles. One flipped byte in the +app and the whole chain refuses. + +### Key Lessons + +1. **Linker scripts place your program.** `MEMORY`/ORIGIN/LENGTH + a changed + `FLASH` line moved the app 128 KiB up. `__StackTop = 0x20082000` in the + vector table is direct evidence. +2. **Modules keep firmware readable.** Both `main.c` files are thin; crypto, + manifest, IR, LED, and banner live in one-responsibility `.c`/`.h` pairs, + every prototype Doxygen-documented, nothing past 8 executable lines. +3. **AES is everywhere in SDK firmware.** `pico_mbedtls` gives you a tuned AES + for free. You used it both to authenticate (bootloader CMAC) and to hide a + string (app CBC banner). +4. **CBC with no padding** encrypts 32 bytes as two blocks; IV is read/updated + in place - give `crypt_cbc` a mutable copy. +5. **Static keys are secrets-in-name only.** SWD/GDB/Ghidra read the key from + flash (`0x10006064`) in minutes. Real products store root keys in OTP and + verify with the ROM (Part 19). +6. **Reverse engineering = treasure hunt.** You never "break" AES; you find + the key, IV, and ciphertext and press decrypt. + +--- + +## Part 19: Your Turn (Hacking Exercises) + +Try these on your own board; each one is a small, safe extension. + +### Challenge 1 - Swap two LED pins without touching the source + +The PINs are value-initialized data in `led_default` (the struct fields the +compiler stored in `.rodata`). Find the address in the stripped app `.bin` +(Ghidra: `led_default` xrefs the literals 16, 17, 18), patch the 16 and 18 +literals, re-flash the *unsigned* image, and watch the bootloader reject it - +then re-sign with `sign_firmware.py` and this time it boots with the hack. + +### Challenge 2 - Change the boot key (both sides) + +Edit the 16 bytes in `verify.c`, rebuild only the bootloader, re-sign the app +with `--key` matching, flash all three, and boot. Then sign the app with the +OLD key and confirm rejection. You have now installed your own signing +authority on your own board. + +### Challenge 3 - Make the banner say something else + +Change the plaintext (must stay 32 bytes for the no-padding CBC), re-encrypt +with `openssl`, update `banner_ct` in `secret.c`, rebuild, re-sign, flash. +Your Ghidra notes from Part 16 now decode the new banner id +to differentiate it. + +### Challenge 4 - Think through the upgrade path + +The manifest has a `version` field that is currently ignored after a magic +check. Sketch how `boot_launch` could validate a counter ("only downgrade a +fixed number of times") - the classic anti-rollback feature that separates +consumer OTA from device management-grade OTA. You should be able to name the +flash sectors you would need to reserve to make that safe (hint: state must +survive app updates, so it cannot live in the app image). + +### Challenge 5 - The honest conversation about the key + +Write one paragraph each for a product manager: (1) why AES-CMAC with a flash +key stops a script-kiddie flash overwrite, and (2) why it does not stop a +determined hardware attacker, and (3) which real mechanism (OTP key, ROM +verifier with `secp256k1`, anti-dumping measurements) closes that hole. If you +can explain all three, you understand secure boot at a professional level. + +--- + +## Key Takeaways + +- Authenticated boot = each stage refuses to start the next until a secret join + the image to a tag proves it is the right bytes. +- AES-128-CMAC (RFC 4493) is a keyed CBC-MAC with two subkeys; unforgeable + without the key, verifiable with a few lines of mbedtls. +- `manifest.bin` is a 4096-byte sector at `0x1001F000`; a 16-byte tag is the + only secret-bearing field. +- The app lives at `0x10020000` because `memmap_app.ld` says so; the vector + table's `0x20082000` stack pointer proves it. +- The SDK bundles Mbed TLS; `mbedtls_aes_*` decrypts at runtime with zero + third-party code. +- OpenOCD `program verify reset exit` writes each partition; + flash read-back `verify` is not authentication. +- GDB `hbreak`/`stepi`/`x/16bx` on a live Pico 2 shows you key bytes and the + exact `bx` that enters the app. +- Ghidra on the ELF names the AES calls; Ghidra on the .bin finds them via the + constant table and IV string. +- A key dumped from flash is not a secret. OTP + ROM verification is. + +--- + +## Glossary + +| Term | Definition | +| ------------------- | ------------------------------------------------------------------- | +| Boot chain | Ordered stages where each stage authenticates the next. | +| XIP | Execute-in-place: code runs straight from flash memory-mapped at 0x10000000. | +| CMAC | Keyed message-authentication code based on a block cipher (NIST CMAC / RFC 4493). | +| CBC-MAC | MAC built by chaining AES-CBC blocks; needs the final-block fix CMAC adds. | +| Tag | The short authenticator output (16 bytes here) proving message integrity. | +| Manifest | Small signed record describing an image: address, size, tag. | +| VTOR | Vector Table Offset Register; tells the CPU where the vector table is. | +| MagnumOpus issue | A willful misunderstanding source: see Part 18. | +| OTP | One-time-programmable memory; writes once, reads forever, cannot be re-flashed. | +| RO/RW/HAL | read only / read write / hardware abstraction layer, the three concerns a linker script separates. | + +--- + +## Additional Resources + +### RFC and Standard References + +- RFC 4493 - The AES-CMAC Algorithm (the subkey derivation and four vectors used here) +- NIST SP 800-38B - Recommendation for Block Cipher Modes of Operation: The CMAC Mode for Authentication + +### In the Repo + +- `tools/sign_firmware.py` - the pure-Python signer/verifier (read the header comment for a CMAC walkthrough) +- `common/include/mbedtls_config.h` - the minimal SDK mbedtls config (AES_C, CBC, CMAC_C) + +### Pico SDK Internals (open these while you read Part 6) + +- `~/.pico-sdk/sdk/2.2.0/src/rp2_common/pico_crt0/rp2350/memmap_default.ld` - the SDK linker script we copied +- `~/.pico-sdk/sdk/2.2.0/src/rp2_common/pico_standard_link/` - crumbs for flash region generation +- `~/.pico-sdk/sdk/2.2.0/src/rp2_common/pico_crt0/crt0.S` - `_reset_handler`, `_entry_point`, and the vector table layout + +### Tooling Used in This Lab + +- OpenOCD from the SDK (`~/.pico-sdk/openocd/0.12.0+dev`) +- GDB / objdump / nm from the ARM GNU toolchain (`~/.pico-sdk/toolchain/14_2_Rel1`) +- Ghidra (import both ELF and raw .bin; see Parts 15-16) +- `openssl enc -aes-128-cbc -nopad` for host-side banner cryptography + +--- + +## Real-World Implications + +### What You've Learned in This Course + +Your Pico 2 is now a small secure-boot platform: ROM -> bootloader -> manifest +-> app, with an authenticated handoff at every boundary you control. The same +skills invert perfectly for defense: when you audit a device, this is the +mental model you bring - *what authenticates the next stage, where is the key, +and what happens if it is not there?* + +### The Power and Responsibility + +- The **power**: you can now build boot chains, move firmware around the + address space at will, extract keys, and re-sign images. That is the exact + toolbox used both to protect smart locks and to compromise them. +- The **responsibility**: only ever apply these techniques to devices you own + 100% or are explicitly authorized to test. The moment you run + `BOOT FAILED` on someone else's device, you have crossed into criminal + tampering and possibly a safety-critical product. +- The **engineering honesty**: a flash key is a speed bump, not a wall. Any + product story that claims "AES secure boot" must be asked, *where is the key, + and who can read it?* If the answer is "in the firmware," the whiteboard does + not add up. + +### Keep Learning + +- Swap the CMAC for the RP2350 ROM `secp256k1` verifier to get real + asymmetric trust (flash dumps stop being useful). +- Use `pico_set_uf2_...` on a real board to ship an OTA payload that your own + bootloader authenticates before the boot stage writes it. +- Read how real device IDs (RP2350's unique serial) can be mixed into the key + schedule so an image signed for one board will not run on another. + +**Now go flash your own CA.** You control the manifest, the key, and the +gate that decides what runs on your hardware. \ No newline at end of file diff --git a/0x0026a_custom-bootloader/WEEK11a.pdf b/0x0026a_custom-bootloader/WEEK11a.pdf new file mode 100644 index 0000000..a3c9d41 Binary files /dev/null and b/0x0026a_custom-bootloader/WEEK11a.pdf differ diff --git a/0x0026a_custom-bootloader/app/include/ir.h b/0x0026a_custom-bootloader/app/include/ir.h new file mode 100644 index 0000000..84aa67f --- /dev/null +++ b/0x0026a_custom-bootloader/app/include/ir.h @@ -0,0 +1,63 @@ +/** + * @file ir.h + * @brief Header for NEC IR receiver (decoder) API + * @author Kevin Thomas + * @date 2025 + * + * MIT License + * + * Copyright (c) 2025 Kevin Thomas + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef IR_H +#define IR_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize the IR receiver GPIO + * + * Configures the given `pin` as an input with pull-up for the NEC IR receiver. + * Call this once during board initialization before calling `ir_getkey()`. + * + * @param pin GPIO pin number connected to IR receiver output + */ +void ir_init(uint8_t pin); + +/** + * @brief Blocking NEC IR decoder + * + * Blocks while waiting for a complete NEC frame. On success returns the + * decoded command byte (0..255). On timeout or protocol error returns -1. + * + * @return decoded command byte (0..255) or -1 on failure + */ +int ir_getkey(void); + +#ifdef __cplusplus +} +#endif + +#endif // IR_H diff --git a/0x0026a_custom-bootloader/app/include/led.h b/0x0026a_custom-bootloader/app/include/led.h new file mode 100644 index 0000000..7e0b63c --- /dev/null +++ b/0x0026a_custom-bootloader/app/include/led.h @@ -0,0 +1,63 @@ +/** + * @file led.h + * @brief LED controller driven by decoded NEC IR commands. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#ifndef LED_H +#define LED_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief LED controller state for the three IR-controlled LEDs. + * + * Maps each logical LED (1-3) to its GPIO pin and remembers whether the + * LED is currently on. Build a default instance with led_default() and + * drive it with led_init() and led_handle_ir(). + */ +typedef struct { + uint8_t led1_pin; + uint8_t led2_pin; + uint8_t led3_pin; + bool led1_state; + bool led2_state; + bool led3_state; +} simple_led_ctrl_t; + +/** + * @brief Build the default three-LED configuration (pins 16-18, all off). + * @param void No parameters. + * @return simple_led_ctrl_t Default LED controller state. + */ +simple_led_ctrl_t led_default(void); + +/** + * @brief Configure the three LED GPIO pins as outputs. + * @param leds Pointer to the LED controller to initialize. + * @return void No return value. + */ +void led_init(simple_led_ctrl_t *leds); + +/** + * @brief Dispatch a decoded NEC command to the matching LED. + * @param leds Pointer to the LED controller to drive. + * @param key Decoded NEC command byte, or a negative value to ignore. + * @return void No return value. + */ +void led_handle_ir(simple_led_ctrl_t *leds, int key); + +#ifdef __cplusplus +} +#endif + +#endif // LED_H \ No newline at end of file diff --git a/0x0026a_custom-bootloader/app/include/secret.h b/0x0026a_custom-bootloader/app/include/secret.h new file mode 100644 index 0000000..124a80e --- /dev/null +++ b/0x0026a_custom-bootloader/app/include/secret.h @@ -0,0 +1,29 @@ +/** + * @file secret.h + * @brief AES-128-CBC protected runtime banner for the application. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#ifndef SECRET_H +#define SECRET_H + +#include + +/** @brief AES block size in bytes used by the runtime banner. */ +#define APP_AES_BLOCK 16U + +/** @brief Length in bytes of the encrypted runtime banner. */ +#define APP_BANNER_LEN 32U + +/** + * @brief Decrypt the embedded AES-128-CBC banner and print it. + * @param void No parameters. + * @return int Zero on success, non-zero when decryption fails. + */ +int secret_print_banner(void); + +#endif diff --git a/0x0026a_custom-bootloader/app/memmap_app.ld b/0x0026a_custom-bootloader/app/memmap_app.ld new file mode 100644 index 0000000..faa2c2c --- /dev/null +++ b/0x0026a_custom-bootloader/app/memmap_app.ld @@ -0,0 +1,305 @@ +/* Based on GCC ARM embedded samples. + Defines the following symbols for use by code: + __exidx_start + __exidx_end + __etext + __data_start__ + __preinit_array_start + __preinit_array_end + __init_array_start + __init_array_end + __fini_array_start + __fini_array_end + __data_end__ + __bss_start__ + __bss_end__ + __end__ + end + __HeapLimit + __StackLimit + __StackTop + __stack (== StackTop) +*/ + +MEMORY +{ + /* Relocated for the custom bootloader: the application is linked to run + from 0x10020000, leaving the first 128k of flash for the bootloader and + its manifest. */ + FLASH(rx) : ORIGIN = 0x10020000, LENGTH = 1920k + RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 512k + SCRATCH_X(rwx) : ORIGIN = 0x20080000, LENGTH = 4k + SCRATCH_Y(rwx) : ORIGIN = 0x20081000, LENGTH = 4k +} + +ENTRY(_entry_point) + +SECTIONS +{ + .flash_begin : { + __flash_binary_start = .; + } > FLASH + + /* The bootrom will enter the image at the point indicated in your + IMAGE_DEF, which is usually the reset handler of your vector table. + + The debugger will use the ELF entry point, which is the _entry_point + symbol, and in our case is *different from the bootrom's entry point.* + This is used to go back through the bootrom on debugger launches only, + to perform the same initial flash setup that would be performed on a + cold boot. + */ + + .text : { + __logical_binary_start = .; + KEEP (*(.vectors)) + KEEP (*(.binary_info_header)) + __binary_info_header_end = .; + KEEP (*(.embedded_block)) + __embedded_block_end = .; + KEEP (*(.reset)) + /* TODO revisit this now memset/memcpy/float in ROM */ + /* bit of a hack right now to exclude all floating point and time critical (e.g. memset, memcpy) code from + * FLASH ... we will include any thing excluded here in .data below by default */ + *(.init) + *libgcc.a:cmse_nonsecure_call.o + *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .text*) + *(.fini) + /* Pull all c'tors into .text */ + *crtbegin.o(.ctors) + *crtbegin?.o(.ctors) + *(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors) + *(SORT(.ctors.*)) + *(.ctors) + /* Followed by destructors */ + *crtbegin.o(.dtors) + *crtbegin?.o(.dtors) + *(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors) + *(SORT(.dtors.*)) + *(.dtors) + + . = ALIGN(4); + /* preinit data */ + PROVIDE_HIDDEN (__preinit_array_start = .); + KEEP(*(SORT(.preinit_array.*))) + KEEP(*(.preinit_array)) + PROVIDE_HIDDEN (__preinit_array_end = .); + + . = ALIGN(4); + /* init data */ + PROVIDE_HIDDEN (__init_array_start = .); + KEEP(*(SORT(.init_array.*))) + KEEP(*(.init_array)) + PROVIDE_HIDDEN (__init_array_end = .); + + . = ALIGN(4); + /* finit data */ + PROVIDE_HIDDEN (__fini_array_start = .); + *(SORT(.fini_array.*)) + *(.fini_array) + PROVIDE_HIDDEN (__fini_array_end = .); + + *(.eh_frame*) + . = ALIGN(4); + } > FLASH + + /* Note the boot2 section is optional, and should be discarded if there is + no reference to it *inside* the binary, as it is not called by the + bootrom. (The bootrom performs a simple best-effort XIP setup and + leaves it to the binary to do anything more sophisticated.) However + there is still a size limit of 256 bytes, to ensure the boot2 can be + stored in boot RAM. + + Really this is a "XIP setup function" -- the name boot2 is historic and + refers to its dual-purpose on RP2040, where it also handled vectoring + from the bootrom into the user image. + */ + + .boot2 : { + __boot2_start__ = .; + *(.boot2) + __boot2_end__ = .; + } > FLASH + + ASSERT(__boot2_end__ - __boot2_start__ <= 256, + "ERROR: Pico second stage bootloader must be no more than 256 bytes in size") + + .rodata : { + *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .rodata*) + *(.srodata*) + . = ALIGN(4); + *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.flashdata*))) + . = ALIGN(4); + } > FLASH + + .ARM.extab : + { + *(.ARM.extab* .gnu.linkonce.armextab.*) + } > FLASH + + __exidx_start = .; + .ARM.exidx : + { + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + } > FLASH + __exidx_end = .; + + /* Machine inspectable binary information */ + . = ALIGN(4); + __binary_info_start = .; + .binary_info : + { + KEEP(*(.binary_info.keep.*)) + *(.binary_info.*) + } > FLASH + __binary_info_end = .; + . = ALIGN(4); + + .ram_vector_table (NOLOAD): { + *(.ram_vector_table) + } > RAM + + .uninitialized_data (NOLOAD): { + . = ALIGN(4); + *(.uninitialized_data*) + } > RAM + + .data : { + __data_start__ = .; + *(vtable) + + *(.time_critical*) + + /* remaining .text and .rodata; i.e. stuff we exclude above because we want it in RAM */ + *(.text*) + . = ALIGN(4); + *(.rodata*) + . = ALIGN(4); + + *(.data*) + *(.sdata*) + + . = ALIGN(4); + *(.after_data.*) + . = ALIGN(4); + /* preinit data */ + PROVIDE_HIDDEN (__mutex_array_start = .); + KEEP(*(SORT(.mutex_array.*))) + KEEP(*(.mutex_array)) + PROVIDE_HIDDEN (__mutex_array_end = .); + + *(.jcr) + . = ALIGN(4); + } > RAM AT> FLASH + + .tdata : { + . = ALIGN(4); + *(.tdata .tdata.* .gnu.linkonce.td.*) + /* All data end */ + __tdata_end = .; + } > RAM AT> FLASH + PROVIDE(__data_end__ = .); + + /* __etext is (for backwards compatibility) the name of the .data init source pointer (...) */ + __etext = LOADADDR(.data); + + .tbss (NOLOAD) : { + . = ALIGN(4); + __bss_start__ = .; + __tls_base = .; + *(.tbss .tbss.* .gnu.linkonce.tb.*) + *(.tcommon) + + __tls_end = .; + } > RAM + + .bss (NOLOAD) : { + . = ALIGN(4); + __tbss_end = .; + + *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.bss*))) + *(COMMON) + PROVIDE(__global_pointer$ = . + 2K); + *(.sbss*) + . = ALIGN(4); + __bss_end__ = .; + } > RAM + + .heap (NOLOAD): + { + __end__ = .; + end = __end__; + KEEP(*(.heap*)) + } > RAM + /* historically on GCC sbrk was growing past __HeapLimit to __StackLimit, however + to be more compatible, we now set __HeapLimit explicitly to where the end of the heap is */ + __HeapLimit = ORIGIN(RAM) + LENGTH(RAM); + + /* Start and end symbols must be word-aligned */ + .scratch_x : { + __scratch_x_start__ = .; + *(.scratch_x.*) + . = ALIGN(4); + __scratch_x_end__ = .; + } > SCRATCH_X AT > FLASH + __scratch_x_source__ = LOADADDR(.scratch_x); + + .scratch_y : { + __scratch_y_start__ = .; + *(.scratch_y.*) + . = ALIGN(4); + __scratch_y_end__ = .; + } > SCRATCH_Y AT > FLASH + __scratch_y_source__ = LOADADDR(.scratch_y); + + /* .stack*_dummy section doesn't contains any symbols. It is only + * used for linker to calculate size of stack sections, and assign + * values to stack symbols later + * + * stack1 section may be empty/missing if platform_launch_core1 is not used */ + + /* by default we put core 0 stack at the end of scratch Y, so that if core 1 + * stack is not used then all of SCRATCH_X is free. + */ + .stack1_dummy (NOLOAD): + { + *(.stack1*) + } > SCRATCH_X + .stack_dummy (NOLOAD): + { + KEEP(*(.stack*)) + } > SCRATCH_Y + + .flash_end : { + KEEP(*(.embedded_end_block*)) + PROVIDE(__flash_binary_end = .); + } > FLASH =0xaa + + /* stack limit is poorly named, but historically is maximum heap ptr */ + __StackLimit = ORIGIN(RAM) + LENGTH(RAM); + __StackOneTop = ORIGIN(SCRATCH_X) + LENGTH(SCRATCH_X); + __StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y); + __StackOneBottom = __StackOneTop - SIZEOF(.stack1_dummy); + __StackBottom = __StackTop - SIZEOF(.stack_dummy); + PROVIDE(__stack = __StackTop); + + /* picolibc and LLVM */ + PROVIDE (__heap_start = __end__); + PROVIDE (__heap_end = __HeapLimit); + PROVIDE( __tls_align = MAX(ALIGNOF(.tdata), ALIGNOF(.tbss)) ); + PROVIDE( __tls_size_align = (__tls_size + __tls_align - 1) & ~(__tls_align - 1)); + PROVIDE( __arm32_tls_tcb_offset = MAX(8, __tls_align) ); + + /* llvm-libc */ + PROVIDE (_end = __end__); + PROVIDE (__llvm_libc_heap_limit = __HeapLimit); + + /* Check if data + heap + stack exceeds RAM limit */ + ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed") + + ASSERT( __binary_info_header_end - __logical_binary_start <= 1024, "Binary info must be in first 1024 bytes of the binary") + ASSERT( __embedded_block_end - __logical_binary_start <= 4096, "Embedded block must be in first 4096 bytes of the binary") + + /* todo assert on extra code */ +} + diff --git a/0x0026a_custom-bootloader/app/src/app_main.c b/0x0026a_custom-bootloader/app/src/app_main.c new file mode 100644 index 0000000..16d9c78 --- /dev/null +++ b/0x0026a_custom-bootloader/app/src/app_main.c @@ -0,0 +1,54 @@ +/** + * @file app_main.c + * @brief Application entry point: IR receiver drives the LED controller. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + * + * ----------------------------------------------------------------------------- + * + * This file is deliberately thin: it only wires the modules together and runs + * the poll loop. The NEC IR decoder lives in ir.c, the LED controller lives in + * led.c, and the AES-128-CBC protected banner lives in secret.c. + * + * Wiring: + * GPIO5 -> IR receiver OUT + * GPIO16 -> LED1 anode (with current-limiting resistor to GND) + * GPIO17 -> LED2 anode (with current-limiting resistor to GND) + * GPIO18 -> LED3 anode (with current-limiting resistor to GND) + */ + +#include +#include +#include "pico/stdlib.h" +#include "ir.h" +#include "led.h" +#include "secret.h" + +/** @brief GPIO pin number for the IR receiver. */ +#define IR_PIN 5 + +/** + * @brief Poll the IR receiver and dispatch decoded keys forever. + * @param leds Pointer to the LED controller to drive. + * @return void Does not return. + */ +static void ir_poll_loop(simple_led_ctrl_t *leds) { + while (true) { + int key = ir_getkey(); + if (key >= 0) led_handle_ir(leds, key); + else sleep_ms(1); + } +} + +int main(void) { + stdio_init_all(); + secret_print_banner(); + simple_led_ctrl_t leds = led_default(); + led_init(&leds); + ir_init(IR_PIN); + printf("IR receiver on GPIO %d ready\n", IR_PIN); + ir_poll_loop(&leds); +} \ No newline at end of file diff --git a/0x0026a_custom-bootloader/app/src/ir.c b/0x0026a_custom-bootloader/app/src/ir.c new file mode 100644 index 0000000..b47835c --- /dev/null +++ b/0x0026a_custom-bootloader/app/src/ir.c @@ -0,0 +1,131 @@ +/** + * @file ir.c + * @brief Implementation of NEC IR receiver (decoder) + * @author Kevin Thomas + * @date 2025 + * + * MIT License + * + * Copyright (c) 2025 Kevin Thomas + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "ir.h" +#include "pico/stdlib.h" +#include "pico/time.h" +#include "hardware/gpio.h" + +/** @brief GPIO pin connected to the IR receiver */ +static unsigned int ir_pin = 0; + +/** + * @brief Wait for a GPIO pin to reach a given logic level + * + * Spins until the pin matches the requested level or a microsecond timeout + * is exceeded. Returns the elapsed time in microseconds, or -1 on timeout. + * + * @param gpio GPIO pin to monitor + * @param level Desired logic level (true = HIGH, false = LOW) + * @param timeout_us Maximum wait in microseconds + * @return int64_t Elapsed microseconds, or -1 on timeout + */ +static int64_t wait_for_level(unsigned int gpio, bool level, uint32_t timeout_us) { + absolute_time_t start = get_absolute_time(); + while (gpio_get(gpio) != level) { + if (absolute_time_diff_us(start, get_absolute_time()) > (int64_t)timeout_us) + return -1; + } + return absolute_time_diff_us(start, get_absolute_time()); +} + +/** + * @brief Wait for the NEC 9 ms leader pulse and 4.5 ms space + * + * @param void No parameters. + * @return bool true if a valid leader was detected, false on timeout + */ +static bool wait_leader(void) { + if (wait_for_level(ir_pin, 0, 150000) < 0) return false; + int64_t t = wait_for_level(ir_pin, 1, 12000); + if (t < 8000 || t > 10000) return false; + t = wait_for_level(ir_pin, 0, 7000); + if (t < 3500 || t > 5000) return false; + return true; +} + +/** + * @brief Read a single NEC-encoded bit from the IR receiver + * + * Measures the mark/space timing and shifts the result into the + * appropriate byte of the data array. + * + * @param data 4-byte array accumulating received bits + * @param i Bit index (0-31) + * @return bool true on success, false on timeout or protocol error + */ +static bool read_nec_bit(uint8_t *data, int i) { + if (wait_for_level(ir_pin, 1, 1000) < 0) return false; + int64_t t = wait_for_level(ir_pin, 0, 2500); + if (t < 200) return false; + int byte_idx = i / 8; + int bit_idx = i % 8; + if (t > 1200) data[byte_idx] |= (1 << bit_idx); + return true; +} + +/** + * @brief Read all 32 data bits of an NEC frame + * + * @param data 4-byte array filled with the received address and command + * @return bool true if all 32 bits were read, false on timeout + */ +static bool read_32_bits(uint8_t *data) { + for (int i = 0; i < 32; ++i) + if (!read_nec_bit(data, i)) return false; + return true; +} + +/** + * @brief Validate an NEC frame and extract the command byte + * + * Checks that the address and command pairs are bitwise-inverted. + * + * @param data 4-byte NEC frame (addr, ~addr, cmd, ~cmd) + * @return int Command byte (0-255) on success, -1 on validation failure + */ +static int validate_nec_frame(const uint8_t *data) { + if ((uint8_t)(data[0] + data[1]) == 0xFF && (uint8_t)(data[2] + data[3]) == 0xFF) + return data[2]; + return -1; +} + +void ir_init(uint8_t pin) { + ir_pin = pin; + gpio_init(pin); + gpio_set_dir(pin, GPIO_IN); + gpio_pull_up(pin); +} + +int ir_getkey(void) { + if (!wait_leader()) return -1; + uint8_t data[4] = {0, 0, 0, 0}; + if (!read_32_bits(data)) return -1; + return validate_nec_frame(data); +} diff --git a/0x0026a_custom-bootloader/app/src/led.c b/0x0026a_custom-bootloader/app/src/led.c new file mode 100644 index 0000000..fee69f9 --- /dev/null +++ b/0x0026a_custom-bootloader/app/src/led.c @@ -0,0 +1,103 @@ +/** + * @file led.c + * @brief LED controller driven by decoded NEC IR commands. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#include +#include "pico/stdlib.h" +#include "led.h" + +/** + * @brief Map an NEC command code to a logical LED number. + * @param ir_command Decoded NEC command byte. + * @return int Logical LED number 1-3, or 0 when the command is unmapped. + */ +static int led_number_for_command(int ir_command) { + if (ir_command == 0x0C) return 1; + if (ir_command == 0x18) return 2; + if (ir_command == 0x5E) return 3; + return 0; +} + +/** + * @brief Return the GPIO pin backing a logical LED number. + * @param leds Pointer to the LED controller. + * @param led_num Logical LED number (1-3). + * @return uint8_t GPIO pin number for the requested LED. + */ +static uint8_t led_pin_for(simple_led_ctrl_t *leds, int led_num) { + if (led_num == 1) return leds->led1_pin; + if (led_num == 2) return leds->led2_pin; + return leds->led3_pin; +} + +/** + * @brief Turn every LED output off. + * @param leds Pointer to the LED controller. + * @return void No return value. + */ +static void led_all_off(simple_led_ctrl_t *leds) { + gpio_put(leds->led1_pin, false); + gpio_put(leds->led2_pin, false); + gpio_put(leds->led3_pin, false); +} + +/** + * @brief Toggle a GPIO on and off for a number of blink cycles. + * @param pin GPIO pin to blink. + * @param count Number of on/off cycles. + * @param delay_ms Milliseconds to hold each phase. + * @return void No return value. + */ +static void led_blink(uint8_t pin, uint8_t count, uint32_t delay_ms) { + for (uint8_t i = 0; i < count; i++) { + gpio_put(pin, true); + sleep_ms(delay_ms); + gpio_put(pin, false); + sleep_ms(delay_ms); + } +} + +/** + * @brief Blink an LED, then leave it lit. + * @param leds Pointer to the LED controller. + * @param led_num Logical LED number to activate. + * @return void No return value. + */ +static void led_activate(simple_led_ctrl_t *leds, int led_num) { + uint8_t pin = led_pin_for(leds, led_num); + led_blink(pin, 3, 50); + gpio_put(pin, true); + printf("LED %d activated on GPIO %d\n", led_num, pin); +} + +simple_led_ctrl_t led_default(void) { + simple_led_ctrl_t leds = { + .led1_pin = 16, .led2_pin = 17, .led3_pin = 18, + .led1_state = false, .led2_state = false, .led3_state = false + }; + return leds; +} + +void led_init(simple_led_ctrl_t *leds) { + gpio_init(leds->led1_pin); + gpio_set_dir(leds->led1_pin, GPIO_OUT); + gpio_init(leds->led2_pin); + gpio_set_dir(leds->led2_pin, GPIO_OUT); + gpio_init(leds->led3_pin); + gpio_set_dir(leds->led3_pin, GPIO_OUT); +} + +void led_handle_ir(simple_led_ctrl_t *leds, int key) { + if (key < 0) return; + printf("NEC command: 0x%02X\n", key); + led_all_off(leds); + int led_num = led_number_for_command(key); + if (led_num == 0) return; + led_activate(leds, led_num); +} \ No newline at end of file diff --git a/0x0026a_custom-bootloader/app/src/secret.c b/0x0026a_custom-bootloader/app/src/secret.c new file mode 100644 index 0000000..8f6df9e --- /dev/null +++ b/0x0026a_custom-bootloader/app/src/secret.c @@ -0,0 +1,88 @@ +/** + * @file secret.c + * @brief Decrypts a runtime banner with the SDK Mbed TLS AES-128-CBC API. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + * + * ----------------------------------------------------------------------------- + * + * This module shows how to use the AES implementation that ships with the Pico + * SDK. A short banner is stored in flash only as AES-128-CBC ciphertext; the + * key and IV live in the same image. At startup secret_print_banner() rebuilds + * the plaintext with mbedtls_aes_crypt_cbc() and writes it to the console. + * + * The plaintext is exactly APP_BANNER_LEN bytes (two whole AES blocks), so no + * padding routine is required. The ciphertext was produced on the host with the + * matching key and IV, for example: + * + * printf '%s' 'SECRET: IR LINK SECURED ONLINE ' \ + * | openssl enc -aes-128-cbc -K 49726f6e4e6574426f6f744b65792121 \ + * -iv 41707053656372657449563132333435 -nopad | xxd -p + */ + +#include +#include +#include "mbedtls/aes.h" +#include "secret.h" + +/** @brief AES-128 key, the same 16 bytes the bootloader uses for CMAC. */ +static const uint8_t banner_key[APP_AES_BLOCK] = { + 0x49, 0x72, 0x6F, 0x6E, 0x4E, 0x65, 0x74, 0x42, + 0x6F, 0x6F, 0x74, 0x4B, 0x65, 0x79, 0x21, 0x21 +}; + +/** @brief Initialization vector used when the banner was encrypted. */ +static const uint8_t banner_iv[APP_AES_BLOCK] = { + 0x41, 0x70, 0x70, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x49, 0x56, 0x31, 0x32, 0x33, 0x34, 0x35 +}; + +/** @brief AES-128-CBC ciphertext of the runtime banner. */ +static const uint8_t banner_ct[APP_BANNER_LEN] = { + 0xBA, 0xEC, 0x4A, 0x19, 0x25, 0x86, 0x1B, 0xB9, + 0x2D, 0xFD, 0x9F, 0x13, 0xF7, 0x9B, 0x3E, 0x80, + 0xCF, 0x12, 0xB6, 0x98, 0xDB, 0x91, 0x65, 0xBE, + 0x8F, 0xD7, 0xC3, 0x47, 0x7B, 0xB2, 0xE7, 0x6E +}; + +/** + * @brief Run the AES-128-CBC decrypt in place over a working buffer. + * @param aes Pointer to an AES context holding an already-expanded key. + * @param iv Pointer to a mutable copy of the initialization vector. + * @param buf Pointer to the buffer holding ciphertext, then plaintext. + * @return int Zero on success, non-zero on failure. + */ +static int secret_cbc(mbedtls_aes_context *aes, uint8_t *iv, uint8_t *buf) { + int rc = mbedtls_aes_crypt_cbc(aes, MBEDTLS_AES_DECRYPT, APP_BANNER_LEN, + iv, buf, buf); + return rc; +} + +/** + * @brief Decrypt the banner ciphertext into a working buffer. + * @param buf Buffer of at least APP_BANNER_LEN bytes. + * @return int Zero on success, non-zero on failure. + */ +static int secret_decrypt(uint8_t *buf) { + mbedtls_aes_context aes; + uint8_t iv[APP_AES_BLOCK]; + memcpy(iv, banner_iv, APP_AES_BLOCK); + mbedtls_aes_init(&aes); + mbedtls_aes_setkey_dec(&aes, banner_key, 128); + int rc = secret_cbc(&aes, iv, buf); + mbedtls_aes_free(&aes); + return rc; +} + +int secret_print_banner(void) { + uint8_t buf[APP_BANNER_LEN + 1]; + memcpy(buf, banner_ct, APP_BANNER_LEN); + buf[APP_BANNER_LEN] = '\0'; + int rc = secret_decrypt(buf); + if (rc != 0) return rc; + printf("app banner: %s\n", (char *)buf); + return 0; +} diff --git a/0x0026a_custom-bootloader/bootloader/include/aes_cmac.h b/0x0026a_custom-bootloader/bootloader/include/aes_cmac.h new file mode 100644 index 0000000..8510e8a --- /dev/null +++ b/0x0026a_custom-bootloader/bootloader/include/aes_cmac.h @@ -0,0 +1,40 @@ +/** + * @file aes_cmac.h + * @brief AES-128-CMAC authentication tag helpers backed by Mbed TLS. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#ifndef AES_CMAC_H +#define AES_CMAC_H + +#include +#include +#include + +/** @brief AES block size in bytes. */ +#define AES_BLOCK_SIZE 16U + +/** + * @brief Compute an AES-128-CMAC tag over a memory region. + * @param key Pointer to the 16-byte AES-128 key. + * @param data Pointer to the message bytes, which may live in XIP flash. + * @param len Message length in bytes. + * @param tag Output buffer that receives the 16-byte CMAC tag. + * @return int Zero on success, non-zero when the cipher is unavailable. + */ +int aes128_cmac(const uint8_t *key, const uint8_t *data, size_t len, + uint8_t *tag); + +/** + * @brief Compare two CMAC tags without short-circuiting on the first byte. + * @param a Pointer to the first tag. + * @param b Pointer to the second tag. + * @return bool True when the tags are byte-for-byte identical. + */ +bool cmac_tags_equal(const uint8_t *a, const uint8_t *b); + +#endif diff --git a/0x0026a_custom-bootloader/bootloader/include/boot.h b/0x0026a_custom-bootloader/bootloader/include/boot.h new file mode 100644 index 0000000..09f6e58 --- /dev/null +++ b/0x0026a_custom-bootloader/bootloader/include/boot.h @@ -0,0 +1,45 @@ +/** + * @file boot.h + * @brief Secure boot console and application handoff helpers. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#ifndef BOOT_H +#define BOOT_H + +#include "manifest.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Bring up the debug console and print the boot banner. + * @param void No parameters. + * @return void No return value. + */ +void boot_init(void); + +/** + * @brief Print a boot failure reason and idle forever. + * @param reason Null-terminated message describing the failure. + * @return void Does not return. + */ +void boot_halt(const char *reason); + +/** + * @brief Announce the verified image and jump to its reset vector. + * @param manifest Pointer to the validated manifest. + * @return void Does not return. + */ +void boot_launch(const fw_manifest_t *manifest); + +#ifdef __cplusplus +} +#endif + +#endif // BOOT_H \ No newline at end of file diff --git a/0x0026a_custom-bootloader/bootloader/include/flash_layout.h b/0x0026a_custom-bootloader/bootloader/include/flash_layout.h new file mode 100644 index 0000000..9338649 --- /dev/null +++ b/0x0026a_custom-bootloader/bootloader/include/flash_layout.h @@ -0,0 +1,31 @@ +/** + * @file flash_layout.h + * @brief Flash partition map shared by the bootloader and the host signer. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#ifndef FLASH_LAYOUT_H +#define FLASH_LAYOUT_H + +#include + +/** @brief Absolute base address of the bootloader image in XIP flash. */ +#define BOOTLOADER_BASE 0x10000000U + +/** @brief Absolute address of the one-sector firmware manifest. */ +#define FW_MANIFEST_BASE 0x1001F000U + +/** @brief Size in bytes of the flash sector holding the manifest. */ +#define FW_MANIFEST_SECTOR_SIZE 4096U + +/** @brief Absolute base address where the signed application runs. */ +#define APP_BASE 0x10020000U + +/** @brief Total XIP flash size in bytes on the RP2350 Pico 2 board. */ +#define FLASH_TOTAL_SIZE 0x200000U + +#endif diff --git a/0x0026a_custom-bootloader/bootloader/include/image.h b/0x0026a_custom-bootloader/bootloader/include/image.h new file mode 100644 index 0000000..5173845 --- /dev/null +++ b/0x0026a_custom-bootloader/bootloader/include/image.h @@ -0,0 +1,23 @@ +/** + * @file image.h + * @brief Control transfer into a freshly verified Cortex-M application. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#ifndef IMAGE_H +#define IMAGE_H + +#include + +/** + * @brief Jump to an application whose vector table starts at a base address. + * @param vector_base Base address of the application vector table. + * @return void Does not return to the caller. + */ +void image_start(uint32_t vector_base); + +#endif diff --git a/0x0026a_custom-bootloader/bootloader/include/manifest.h b/0x0026a_custom-bootloader/bootloader/include/manifest.h new file mode 100644 index 0000000..2bc5336 --- /dev/null +++ b/0x0026a_custom-bootloader/bootloader/include/manifest.h @@ -0,0 +1,51 @@ +/** + * @file manifest.h + * @brief Firmware manifest stored in the flash sector before the application. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#ifndef MANIFEST_H +#define MANIFEST_H + +#include +#include + +/** @brief Little-endian magic at the head of a valid manifest ("WFLB"). */ +#define FW_MANIFEST_MAGIC 0x424C4657U + +/** @brief Current manifest structure revision. */ +#define FW_MANIFEST_VERSION 1U + +/** @brief Length in bytes of an AES-128-CMAC authentication tag. */ +#define FW_TAG_SIZE 16U + +#pragma pack(push, 1) +/** @brief Fixed-layout record describing the signed application image. */ +typedef struct { + uint32_t magic; + uint32_t version; + uint32_t app_base; + uint32_t app_size; + uint8_t tag[FW_TAG_SIZE]; +} fw_manifest_t; +#pragma pack(pop) + +/** + * @brief Copy the manifest out of its fixed flash address. + * @param out Pointer to the manifest structure to populate. + * @return void No return value. + */ +void manifest_read(fw_manifest_t *out); + +/** + * @brief Report whether a manifest carries the expected magic and version. + * @param manifest Pointer to the manifest to inspect. + * @return bool True when the magic and version both match. + */ +bool manifest_is_valid(const fw_manifest_t *manifest); + +#endif diff --git a/0x0026a_custom-bootloader/bootloader/include/verify.h b/0x0026a_custom-bootloader/bootloader/include/verify.h new file mode 100644 index 0000000..5734107 --- /dev/null +++ b/0x0026a_custom-bootloader/bootloader/include/verify.h @@ -0,0 +1,32 @@ +/** + * @file verify.h + * @brief Firmware authenticity verification for the secure bootloader. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#ifndef VERIFY_H +#define VERIFY_H + +#include +#include "manifest.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Recompute the CMAC over the application and compare it to the tag. + * @param manifest Pointer to the validated manifest. + * @return bool True when the tag matches, false otherwise. + */ +bool verify_manifest(const fw_manifest_t *manifest); + +#ifdef __cplusplus +} +#endif + +#endif // VERIFY_H \ No newline at end of file diff --git a/0x0026a_custom-bootloader/bootloader/src/aes_cmac.c b/0x0026a_custom-bootloader/bootloader/src/aes_cmac.c new file mode 100644 index 0000000..9dcc481 --- /dev/null +++ b/0x0026a_custom-bootloader/bootloader/src/aes_cmac.c @@ -0,0 +1,29 @@ +/** + * @file aes_cmac.c + * @brief AES-128-CMAC primitives implemented on top of the SDK Mbed TLS. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#include "mbedtls/cmac.h" +#include "mbedtls/cipher.h" +#include "aes_cmac.h" + +int aes128_cmac(const uint8_t *key, const uint8_t *data, size_t len, + uint8_t *tag) { + const mbedtls_cipher_info_t *info; + info = mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_128_ECB); + if (info == NULL) return -1; + return mbedtls_cipher_cmac(info, key, 128, data, len, tag); +} + +bool cmac_tags_equal(const uint8_t *a, const uint8_t *b) { + uint8_t diff = 0; + for (size_t i = 0; i < AES_BLOCK_SIZE; i++) { + diff |= (uint8_t)(a[i] ^ b[i]); + } + return diff == 0; +} diff --git a/0x0026a_custom-bootloader/bootloader/src/boot.c b/0x0026a_custom-bootloader/bootloader/src/boot.c new file mode 100644 index 0000000..0b1dcb1 --- /dev/null +++ b/0x0026a_custom-bootloader/bootloader/src/boot.c @@ -0,0 +1,33 @@ +/** + * @file boot.c + * @brief Secure boot console and application handoff helpers. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#include +#include "pico/stdlib.h" +#include "boot.h" +#include "image.h" + +void boot_init(void) { + stdio_init_all(); + sleep_ms(2000); + printf("\n=== 0x0026a secure bootloader ===\n"); +} + +void boot_halt(const char *reason) { + printf("BOOT FAILED: %s\n", reason); + while (true) { + sleep_ms(500); + } +} + +void boot_launch(const fw_manifest_t *manifest) { + printf("signature valid, entering app at 0x%08lx\n", + (unsigned long)manifest->app_base); + image_start(manifest->app_base); +} \ No newline at end of file diff --git a/0x0026a_custom-bootloader/bootloader/src/image.c b/0x0026a_custom-bootloader/bootloader/src/image.c new file mode 100644 index 0000000..8e3a5f3 --- /dev/null +++ b/0x0026a_custom-bootloader/bootloader/src/image.c @@ -0,0 +1,33 @@ +/** + * @file image.c + * @brief Loads the application stack pointer and branches to its reset vector. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#include "image.h" + +/** @brief Application reset handler signature. */ +typedef void (*image_entry_t)(void); + +/** + * @brief Mask interrupts and point VTOR at the incoming image. + * @param base Vector table base address written to the VTOR register. + * @return void No return value. + */ +static void halt_irq_and_rebase(uint32_t base) { + __asm volatile ("cpsid i" ::: "memory"); + *(volatile uint32_t *)0xE000ED08U = base; +} + +void image_start(uint32_t vector_base) { + uint32_t *vectors = (uint32_t *)vector_base; + image_entry_t entry = (image_entry_t)vectors[1]; + halt_irq_and_rebase(vector_base); + __asm volatile ("msr msp, %0" :: "r" (vectors[0]) : "memory"); + __asm volatile ("dsb\n\tisb" ::: "memory"); + entry(); +} diff --git a/0x0026a_custom-bootloader/bootloader/src/main.c b/0x0026a_custom-bootloader/bootloader/src/main.c new file mode 100644 index 0000000..044abf5 --- /dev/null +++ b/0x0026a_custom-bootloader/bootloader/src/main.c @@ -0,0 +1,34 @@ +/** + * @file main.c + * @brief Secure bootloader entry point (verify AES-128-CMAC, then boot). + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + * + * ----------------------------------------------------------------------------- + * + * This file is deliberately thin: it only sequences the secure boot steps. + * The console setup, halt reporting, and application handoff live in boot.c; + * the CMAC verification (and its key) live in verify.c; the manifest lives in + * manifest.c; and the raw reset-vector jump lives in image.c. + * + * Flash map (2 MiB, RP2350 Pico 2): + * 0x10000000 bootloader image (this program) + * 0x1001F000 one-sector firmware manifest (magic, size, CMAC tag) + * 0x10020000 signed application image + */ + +#include "manifest.h" +#include "boot.h" +#include "verify.h" + +int main(void) { + fw_manifest_t manifest; + boot_init(); + manifest_read(&manifest); + if (!manifest_is_valid(&manifest)) boot_halt("manifest invalid"); + if (!verify_manifest(&manifest)) boot_halt("firmware signature invalid"); + boot_launch(&manifest); +} \ No newline at end of file diff --git a/0x0026a_custom-bootloader/bootloader/src/manifest.c b/0x0026a_custom-bootloader/bootloader/src/manifest.c new file mode 100644 index 0000000..90514c1 --- /dev/null +++ b/0x0026a_custom-bootloader/bootloader/src/manifest.c @@ -0,0 +1,23 @@ +/** + * @file manifest.c + * @brief Reads and validates the firmware manifest from XIP flash. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#include "flash_layout.h" +#include "manifest.h" + +void manifest_read(fw_manifest_t *out) { + const fw_manifest_t *src = (const fw_manifest_t *)FW_MANIFEST_BASE; + *out = *src; +} + +bool manifest_is_valid(const fw_manifest_t *manifest) { + if (manifest->magic != FW_MANIFEST_MAGIC) return false; + if (manifest->version != FW_MANIFEST_VERSION) return false; + return true; +} diff --git a/0x0026a_custom-bootloader/bootloader/src/verify.c b/0x0026a_custom-bootloader/bootloader/src/verify.c new file mode 100644 index 0000000..08b55cd --- /dev/null +++ b/0x0026a_custom-bootloader/bootloader/src/verify.c @@ -0,0 +1,28 @@ +/** + * @file verify.c + * @brief Firmware authenticity verification for the secure bootloader. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + */ + +#include +#include "flash_layout.h" +#include "verify.h" +#include "aes_cmac.h" + +/** @brief AES-128 key that authenticates the application image. */ +static const uint8_t fw_key[FW_TAG_SIZE] = { + 0x49, 0x72, 0x6F, 0x6E, 0x4E, 0x65, 0x74, 0x42, + 0x6F, 0x6F, 0x74, 0x4B, 0x65, 0x79, 0x21, 0x21 +}; + +bool verify_manifest(const fw_manifest_t *manifest) { + const uint8_t *base = (const uint8_t *)manifest->app_base; + uint8_t computed[FW_TAG_SIZE]; + int rc = aes128_cmac(fw_key, base, manifest->app_size, computed); + if (rc != 0) return false; + return cmac_tags_equal(computed, manifest->tag); +} \ No newline at end of file diff --git a/0x0026a_custom-bootloader/common/include/mbedtls_config.h b/0x0026a_custom-bootloader/common/include/mbedtls_config.h new file mode 100644 index 0000000..12ec622 --- /dev/null +++ b/0x0026a_custom-bootloader/common/include/mbedtls_config.h @@ -0,0 +1,25 @@ +/** + * @file mbedtls_config.h + * @brief Minimal Mbed TLS configuration for the 0x0026a bootloader and app. + * @author Kevin Thomas + * @date 2026 + * + * MIT License + * Copyright (c) 2026 Kevin Thomas + * + * The Pico SDK points MBEDTLS_CONFIG_FILE at pico_mbedtls_config.h, which in + * turn includes this file. Only the AES-128 building blocks needed for CMAC + * authentication and CBC encryption are enabled, keeping the image small. + */ + +#ifndef MBEDTLS_CONFIG_H +#define MBEDTLS_CONFIG_H + +#include + +#define MBEDTLS_AES_C +#define MBEDTLS_CIPHER_C +#define MBEDTLS_CIPHER_MODE_CBC +#define MBEDTLS_CMAC_C + +#endif diff --git a/0x0026a_custom-bootloader/manifest.bin b/0x0026a_custom-bootloader/manifest.bin new file mode 100644 index 0000000..7b734c4 Binary files /dev/null and b/0x0026a_custom-bootloader/manifest.bin differ diff --git a/0x0026a_custom-bootloader/pico_sdk_import.cmake b/0x0026a_custom-bootloader/pico_sdk_import.cmake new file mode 100644 index 0000000..d493cc2 --- /dev/null +++ b/0x0026a_custom-bootloader/pico_sdk_import.cmake @@ -0,0 +1,121 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd. +# +# 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 of the copyright holder 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 HOLDER 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. + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG)) + set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG}) + message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')") +endif () + +if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG) + set(PICO_SDK_FETCH_FROM_GIT_TAG "master") + message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG") +endif() + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") +set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + ) + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Populate( + pico_sdk + QUIET + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + GIT_SUBMODULES_RECURSE FALSE + + SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src + BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build + SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild + ) + else () + FetchContent_Populate( + pico_sdk + QUIET + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + + SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src + BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build + SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild + ) + endif () + + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/0x0026a_custom-bootloader/tools/sign_firmware.py b/0x0026a_custom-bootloader/tools/sign_firmware.py new file mode 100644 index 0000000..014a3a0 --- /dev/null +++ b/0x0026a_custom-bootloader/tools/sign_firmware.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Sign a Raspberry Pi Pico application image for the 0x0026a bootloader. + +The signer reads the raw application binary (the bytes the bootloader will see +in XIP flash), computes an AES-128-CMAC tag over it, and writes the fixed-layout +manifest the bootloader expects into the sector at 0x1001F000. The AES and CMAC +routines are implemented here so the tool runs on a stock Python 3 install with +no third-party packages. + +Examples +-------- +Sign an image and write the manifest:: + + python3 sign_firmware.py build/0x0026a_app.bin --out build/manifest.bin + +Verify an existing manifest against an image:: + + python3 sign_firmware.py build/0x0026a_app.bin --verify build/manifest.bin +""" + +from __future__ import annotations + +import argparse +import struct +from pathlib import Path + +MANIFEST_MAGIC = 0x424C4657 +MANIFEST_VERSION = 1 +MANIFEST_SECTOR_SIZE = 4096 +DEFAULT_APP_BASE = 0x10020000 +DEFAULT_KEY = b"IronNetBootKey!!" +RB = 0x1B +RB_128 = 0x87 + +SBOX = ( + 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, + 0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, + 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26, + 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, + 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, + 0xEB, 0x27, 0xB2, 0x75, 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, + 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, 0x53, 0xD1, 0x00, 0xED, + 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, + 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, + 0x50, 0x3C, 0x9F, 0xA8, 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, + 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, 0xCD, 0x0C, 0x13, 0xEC, + 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, + 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, + 0xDE, 0x5E, 0x0B, 0xDB, 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, + 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, 0xE7, 0xC8, 0x37, 0x6D, + 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, + 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, + 0x4B, 0xBD, 0x8B, 0x8A, 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, + 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, 0xE1, 0xF8, 0x98, 0x11, + 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, + 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, + 0xB0, 0x54, 0xBB, 0x16, +) + +RCON = (0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36) + + +def _xtime(value: int) -> int: + """Multiply a byte by x in GF(2^8).""" + value <<= 1 + return (value ^ RB) & 0xFF if value & 0x100 else value + + +def _expand_key(key: bytes) -> list[int]: + """Expand a 16-byte key into the 176-byte AES-128 round key schedule.""" + schedule = list(key) + for index in range(16, 176, 4): + word = schedule[index - 4:index] + if index % 16 == 0: + word = word[1:] + word[:1] + word = [SBOX[byte] for byte in word] + word[0] ^= RCON[index // 16 - 1] + for offset in range(4): + schedule.append(schedule[index - 16 + offset] ^ word[offset]) + return schedule + + +def _add_round_key(state: list[int], schedule: list[int]) -> None: + """XOR the round key into the state in place.""" + for index in range(16): + state[index] ^= schedule[index] + + +def _sub_bytes(state: list[int]) -> None: + """Apply the AES S-box to every byte of the state.""" + for index in range(16): + state[index] = SBOX[state[index]] + + +def _shift_rows(state: list[int]) -> None: + """Rotate each row of the AES state left by its row index.""" + for row in range(1, 4): + cells = [state[row + 4 * col] for col in range(4)] + cells = cells[row:] + cells[:row] + for col in range(4): + state[row + 4 * col] = cells[col] + + +def _mix_columns(state: list[int]) -> None: + """Mix each column of the AES state with the fixed AES matrix.""" + for col in range(4): + base = 4 * col + a0, a1, a2, a3 = state[base:base + 4] + total = a0 ^ a1 ^ a2 ^ a3 + state[base + 0] = a0 ^ total ^ _xtime(a0 ^ a1) + state[base + 1] = a1 ^ total ^ _xtime(a1 ^ a2) + state[base + 2] = a2 ^ total ^ _xtime(a2 ^ a3) + state[base + 3] = a3 ^ total ^ _xtime(a3 ^ a0) + + +def aes_encrypt_block(block: bytes, schedule: list[int]) -> bytes: + """Encrypt one 16-byte block with the supplied round key schedule.""" + state = list(block) + _add_round_key(state, schedule) + for round_index in range(1, 10): + _sub_bytes(state) + _shift_rows(state) + _mix_columns(state) + _add_round_key(state, schedule[16 * round_index:]) + _sub_bytes(state) + _shift_rows(state) + _add_round_key(state, schedule[160:]) + return bytes(state) + + +def _double(block: bytes) -> bytes: + """Left-shift a 128-bit big-endian value, folding in the CMAC constant.""" + value = int.from_bytes(block, "big") + value <<= 1 + if block[0] & 0x80: + value ^= RB_128 + return (value & ((1 << 128) - 1)).to_bytes(16, "big") + + +def _subkeys(schedule: list[int]) -> tuple[bytes, bytes]: + """Derive the RFC 4493 K1 and K2 subkeys.""" + k1 = _double(aes_encrypt_block(bytes(16), schedule)) + return k1, _double(k1) + + +def _xor(left: bytes, right: bytes) -> bytes: + """XOR two equal-length byte strings.""" + return bytes(a ^ b for a, b in zip(left, right)) + + +def aes128_cmac(key: bytes, message: bytes) -> bytes: + """Return the RFC 4493 AES-128-CMAC tag of a message.""" + schedule = _expand_key(key) + k1, k2 = _subkeys(schedule) + blocks = [message[i:i + 16] for i in range(0, len(message), 16)] + complete = bool(blocks) and len(blocks[-1]) == 16 + if not complete: + tail = (blocks[-1] + b"\x80") if blocks else b"\x80" + blocks = blocks[:-1] + [tail.ljust(16, b"\x00")] + last = blocks[-1] + last = _xor(last, k1 if complete else k2) + state = bytes(16) + for block in blocks[:-1]: + state = aes_encrypt_block(_xor(state, block), schedule) + return aes_encrypt_block(_xor(state, last), schedule) + + +def build_manifest(app: bytes, app_base: int, key: bytes) -> bytes: + """Assemble the padded manifest sector for an application image.""" + tag = aes128_cmac(key, app) + header = struct.pack(" bytes: + """Decode a hex or UTF-8 key argument into 16 bytes.""" + raw = bytes.fromhex(text) if len(text) == 32 else text.encode() + if len(raw) != 16: + raise argparse.ArgumentTypeError("key must be 16 bytes") + return raw + + +def _read_image(path: str) -> bytes: + """Read an application binary from disk.""" + data = Path(path).read_bytes() + if not data: + raise SystemExit(f"error: {path} is empty") + return data + + +def _cmd_sign(args: argparse.Namespace) -> int: + """Sign an image and write the manifest sector.""" + app = _read_image(args.image) + manifest = build_manifest(app, args.app_base, args.key) + out = Path(args.out) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_bytes(manifest) + tag = manifest[16:32].hex() + print(f"image : {args.image} ({len(app)} bytes)") + print(f"app_base : 0x{args.app_base:08x}") + print(f"cmac tag : {tag}") + print(f"manifest : {out} ({len(manifest)} bytes)") + return 0 + + +def _cmd_verify(args: argparse.Namespace) -> int: + """Recompute the tag and compare it against a manifest on disk.""" + app = _read_image(args.image) + manifest = Path(args.verify).read_bytes() + stored = manifest[16:32] + expected = aes128_cmac(args.key, app) + print(f"stored : {stored.hex()}") + print(f"computed : {expected.hex()}") + if stored != expected: + print("result : MISMATCH") + return 1 + print("result : MATCH") + return 0 + + +def main() -> int: + """Parse arguments and dispatch to sign or verify.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("image", help="raw application .bin to sign") + parser.add_argument("--out", default="manifest.bin", + help="manifest output path (default: manifest.bin)") + parser.add_argument("--app-base", type=lambda v: int(v, 0), + default=DEFAULT_APP_BASE, + help="application base address (default: 0x10020000)") + parser.add_argument("--key", type=_parse_key, default=DEFAULT_KEY, + help="16-byte AES key as text or 32 hex digits") + parser.add_argument("--verify", metavar="MANIFEST", + help="verify an existing manifest instead of signing") + args = parser.parse_args() + if args.verify: + return _cmd_verify(args) + return _cmd_sign(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/README.md b/README.md index 82e68a7..db0ab0d 100644 --- a/README.md +++ b/README.md @@ -579,6 +579,18 @@ Structures and Functions in Embedded Systems: Debugging and Hacking w/ IR Remote ## Week 11-IDA Notebook [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK11/WEEK11-IDA.md) +## Week 11a Secure Boot + Custom Bootloader Notebook [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/WEEK11a.md) + +### 0x0026a Custom Secure Bootloader [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/README.md) + +### 0x0026a Bootloader BIN [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/0x0026a_bootloader.bin) + +### 0x0026a Bootloader UF2 [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/0x0026a_bootloader.uf2) + +### 0x0026a Signed Application BIN [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/0x0026a_app.bin) + +### 0x0026a Manifest BIN [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/manifest.bin) + ### Chapter 35: Structures This chapter covers structures as well as an intro to infrared basics as we work a infrared receiver and infrared remote controller as it relates to embedded development on the Pico 2.