Skip to content

oxidecomputer/bootleby

Repository files navigation

Bootleby: A LPC55 Bootloader

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.

Really minimal user manual

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.

Building

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.

Testing

We have some minimal test suites in the form of other binaries in src/bin. Currently, this only covers the crypto hardware drivers.

Dev workflow

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.

Demo using fake keys and signed images

This has been tested using the LPCXpresso55S69 board.

Getting Started

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

Building bootleby and extracting a bin file

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

Generating a signed bootleby

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).

Generating A/B images

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.

Erasing the flash and any existing keys

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

Loading the keys and CMPA/CFPA contents

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

Programming our images

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

Trying it out and troubleshooting

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.

Rewriting just one image

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.

Getting DICE to work

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,

  1. Set a breakpoint at main in bootleby.
  2. Dump 8 registers starting at 0x5000_0900 (in pyocd: read32 0x50000900 32)
  3. Resume the program and let it boot
  4. Halt it again.
  5. 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.