Status: approaching feature-complete for now.
This is a reimplementation of Oxide's "stage0" LPC55 bootloader that aims to be more minimal and general.
The reason we need a bootloader is due to a limitation in the chip's boot ROM: it will perform verified boot against a single location in flash (address zero). We need A/B firmware images so that we can do an in-system upgrade safely, but want to retain verified boot. So, this bootloader extends the ROM's verified boot chain of trust to one of two possible firmware images, depending on the results of signature checks and some minimal online configuration.
This will not cover how to get verified boot working on an LPC55; you need to have already installed your key table hash in the CMPA, etc.
bootleby
should be built and signed and deposited at the start of flash. By
default, it structures the rest of flash into two image slots, each 256 kiB in
length. The first starts at 0x1_0000
, and the second at 0x5_0000
.
bootleby
will do exactly the following:
-
Check each firmware slot to see if it contains a valid, fully programmed, signed image. The ROM signature check logic is reused, though we perform some checks before it to work around some crash-level bugs it contains (sigh).
-
If only one slot is valid, boot it (i.e. load its initial stack pointer, configure its vector table, and jump to its reset vector).
-
If two slots are valid, we have a choice to make. The following factors are used to break the tie, in this order:
- If implemented on the target board, an override button.
- A location in RAM is checked for a "transient boot override" command, which will boot into a particular slot exactly once and then be overwritten.
- The LSB of the word at offset
0x100
in the most recently written CFPA page is the last option. If 0, slot A is booted; if 1, slot B is booted.
bootleby
does not stay resident or require any ongoing resources once it jumps
into your program. This means there's no way to "call into" bootleby
other than
rebooting.
cargo build --release
will build for the default target board (which is an
eval board you probably don't have).
To change boards, check Cargo.toml
for the various target-board-*
features,
and pass one like this:
cargo build --release --no-default-features --features target-board-lpcxpresso
To enable booting unsigned-but-CRC'd images, add the allow-unsigned-images
feature.
We have some minimal test suites in the form of other binaries in src/bin
.
Currently, this only covers the crypto hardware drivers.
rust-analyzer
works in this codebase and I intend to keep it that way. Make
sure you've installed the one corresponding to the pinned toolchain, for good
measure.
The main
branch is protected, code must pass the build before being pushed
there.
This code is structured as a lib crate (most of the files under src/
) so that
it can be reused by both the real production bootleby binary, and tests.
Production bootleby lives at src/bin/bootleby.rs
; other files in src/bin
are
test programs. Ideally, more of the code would move into the lib and get tested;
the only parts that really can't are the ones that boot into the chosen image.
This has been tested using the LPCXpresso55S69 board.
You will need the lpc55_support
crate checked out.
-
Install a jumper on P1 (near the Debug Link USB connector) to break serial connection to the LPCLINK2 because I couldn't get it to work reliably.
-
Connect a USB cable to the Debug Link connector for power (and GDB if you need it).
-
Connect a 3.3V serial cable to P8 (all the way across the board to the right), keeping in mind that the legend is from the host's perspective (e.g. TX is coming from the host to the LPC55).
-
Press and hold the ISP button (far right), and press and release the Reset button while holding ISP.
In the lpc55_support
crate, run this command to check if you're successfully
talking to the bootloader (replace /dev/ttyUSB0
with the name of your serial
adapter):
cargo run --bin lpc55_flash /dev/ttyUSB0 ping
In this repo, run
cargo build --release --no-default-features --features target-board-lpcxpresso
arm-none-eabi-objcopy -O binary target/thumbv8m.main-none-eabihf/release/bootleby bootleby.bin
All commands in this section must be executed inside your lpc55_support
checkout, and must all be prefixed with cargo run --bin
. I just got tired
of repeating that part.
These commands will reference files in the bootleby repo (home of this README
file). I'll symbolically represent that path as $BOOTLEBY
below. Either define
an environment variable with the path, or replace it in the commands.
lpc55_sign signed-image $BOOTLEBY/bootleby.bin \
$BOOTLEBY/demo/fake_private_key.pem \
$BOOTLEBY/demo/fake_certificate.der.crt \
$BOOTLEBY/bootleby_signed.bin \
$BOOTLEBY/bootleby_cmpa.bin \
--cfpa $BOOTLEBY/bootleby_cfpa.bin \
This will produce two files in $BOOTLEBY
: bootleby_signed.bin
is a signed
version of the bootleby build, and bootleby_cmpa.bin
is an image containing the
keys to be programmed into the chip's Customer Manufacturing Parameter Area
(CMPA).
Two demo images are included in $BOOTLEBY/demo
, slot_a.bin
and slot_b.bin
.
The slot A image will turn the LED green if it boots; the B image will turn it
blue. This way you can tell them apart.
Currently bootleby is configured to boot either correctly signed images, or unsigned images with a correct NXP CRC. Let's make one of each!
From lpc55_support
, sign the demo slot A program using the same key as before,
discarding the CMPA output, and specifying the program's load address (since
we're using bin files, the load addresses are lost):
lpc55_sign signed-image $BOOTLEBY/demo/slot_a.bin \
$BOOTLEBY/demo/fake_private_key.pem \
$BOOTLEBY/demo/fake_certificate.der.crt \
$BOOTLEBY/demo/slot_a_signed.bin \
/dev/null \
--address 0x10000
Then, wrap the slot B program in a CRC image without a signature:
lpc55_sign crc $BOOTLEBY/demo/slot_b.bin \
$BOOTLEBY/demo/slot_b_crc.bin \
--address 0x50000
You should now have slot_a_signed.bin
and slot_b_crc.bin
files in
$BOOTLEBY/demo
.
For good measure let's begin by putting the chip into a fairly pristine state. This isn't strictly necessary if your board is brand new but doesn't hurt anything.
lpc55_flash /dev/ttyUSB0 erase-cmpa
lpc55_flash /dev/ttyUSB0 flash-erase-all
This will overwrite the newly-erased CMPA with our certificate:
lpc55_flash /dev/ttyUSB0 write-cmpa $BOOTLEBY/bootleby_cmpa.bin
lpc55_flash /dev/ttyUSB0 write-cfpa $BOOTLEBY/bootleby_cfpa.bin
We now have three separate images that need to be placed at three separate areas
in Flash. We can do that by running the following three commands (in
lpc55_support
):
lpc55_flash /dev/ttyUSB0 write-memory 0 $BOOTLEBY/bootleby_signed.bin
lpc55_flash /dev/ttyUSB0 write-memory 0x10000 $BOOTLEBY/demo/slot_a_signed.bin
lpc55_flash /dev/ttyUSB0 write-memory 0x50000 $BOOTLEBY/demo/slot_b_crc.bin
Hit the RESET button.
You should see the LED light green. This means slot A has been booted, which means that bootleby successfully verified its signature and chain-loaded it.
Try holding the USER button while you reset the board. The LED should light blue. This is because bootleby is currently configured to treat the USER button as an image selection override and prefer slot B if both are present.
If something goes wrong, you'll see one of two results:
-
LED lights red: bootleby found no valid images or panicked while verifying them. Try performing the steps above again, making sure you didn't skip anything. If it still doesn't work please report it.
-
LED lights blue without USER button held: bootleby doesn't believe slot A is signed correctly. This usually means you forgot to load the keys into the CMPA, but could also be an error in the sign command (in particular, make sure the load address is specified as given above).
-
LED does not turn on: bootleby has failed to start, and is probably sitting in a HardFault handler. This usually happens because you failed to program either slot A or slot B; bootleby will currently crash if it reads erased flash. If both slots are programmed, please report this.
For bootleby:
lpc55_flash /dev/ttyUSB0 flash-erase-region 0 0x10000
lpc55_flash /dev/ttyUSB0 write-memory 0 $BOOTLEBY/bootleby_signed.bin
For slot A:
lpc55_flash /dev/ttyUSB0 flash-erase-region 0x10000 0x40000
lpc55_flash /dev/ttyUSB0 write-memory 0x10000 $BOOTLEBY/demo/slot_a_signed.bin
For slot B:
lpc55_flash /dev/ttyUSB0 flash-erase-region 0x50000 0x40000
lpc55_flash /dev/ttyUSB0 write-memory 0x50000 $BOOTLEBY/demo/slot_b_crc.bin
This also gives you the opportunity to experiment with installing signed, unsigned, or even entirely bogus images to see what bootleby does.
If you erase slot A or B and attempt to boot, bootleby will currently crash (see above). If you erase bootleby and attempt to boot, you'll end up in the bootloader.
If you follow the instructions above, we will provide a DICE CDI to the next-stage software, but it will be based solely on the next stage software and won't include any measurement of the ROM or bootleby. This is because the ROM DICE support is turned off by default. Enabling it takes some doing.
You will need:
- A CMPA image indicating that DICE should be enabled.
- A valid CFPA image.
- To enroll the PUF and generate the UDS.
These instructions assume $BOOTLEBY/bootleby.bin
is the binary bootleby extracted
during a previous build step. We need to sign it slightly differently to get
the CFPA and then run some other steps. From lpc55_support
, and again
prefixing each command with cargo run --bin
, do:
lpc55_sign signed-image $BOOTLEBY/bootleby.bin \
$BOOTLEBY/demo/fake_private_key.pem \
$BOOTLEBY/demo/fake_certificate.der.crt \
$BOOTLEBY/bootleby_signed.bin \
$BOOTLEBY/bootleby_cmpa.bin \
--cfpa $BOOTLEBY/bootleby_cfpa.bin \
--with-dice \
--with-dice-inc-nxp-cfg \
--with-dice-cust-cfg \
--with-dice-inc-sec-epoch
lpc55_flash /dev/ttyUSB0 erase-cmpa
lpc55_flash /dev/ttyUSB0 write-cmpa $BOOTLEBY/bootleby_cmpa.bin
lpc55_flash /dev/ttyUSB0 write-cfpa \
$BOOTLEBY/bootleby_cfpa.bin \
--update-version
lpc55_flash /dev/ttyUSB0 erase-key-store
lpc55_flash /dev/ttyUSB0 enroll
lpc55_flash /dev/ttyUSB0 generate-uds
lpc55_flash /dev/ttyUSB0 write-key-store
If you reboot, it should behave exactly the same as the previous signed bootleby. Currently you need a debugger to tell the difference. To verify that DICE is on,
- Set a breakpoint at
main
in bootleby. - Dump 8 registers starting at
0x5000_0900
(in pyocd:read32 0x50000900 32
) - Resume the program and let it boot
- Halt it again.
- Dump the same registers.
In both dumps, the registers should appear random, and the second dump should be different from the first.
If DICE is not successfully enabled, the first dump will be all zeros.