diff --git a/.github/workflows/cargo_check.yml b/.github/workflows/cargo_check.yml index 5681368..185b028 100644 --- a/.github/workflows/cargo_check.yml +++ b/.github/workflows/cargo_check.yml @@ -82,3 +82,9 @@ jobs: with: command: check args: --target thumbv7em-none-eabi --release --examples + + - name: Check bootloader + uses: actions-rs/cargo@v1 + with: + command: check + args: --manifest-path bootloader/Cargo.toml --target thumbv7em-none-eabi --release diff --git a/.github/workflows/cargo_fmt.yml b/.github/workflows/cargo_fmt.yml index b17f60c..e327f17 100644 --- a/.github/workflows/cargo_fmt.yml +++ b/.github/workflows/cargo_fmt.yml @@ -71,3 +71,9 @@ jobs: with: command: fmt args: --manifest-path tools/heapviz/Cargo.toml --all -- --check + + - name: Cargo format bootloader + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --manifest-path bootloader/Cargo.toml --all -- --check diff --git a/bootloader/.cargo/config b/bootloader/.cargo/config new file mode 100644 index 0000000..524503b --- /dev/null +++ b/bootloader/.cargo/config @@ -0,0 +1,2 @@ +[target.thumbv7em-none-eabi] +linker = "arm-none-eabi-gcc" diff --git a/bootloader/Cargo.toml b/bootloader/Cargo.toml new file mode 100644 index 0000000..bebe244 --- /dev/null +++ b/bootloader/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "bootloader" +version = "0.1.0" +authors = [ + "Fabian Kaczmarczyck ", +] +build = "build.rs" +license = "Apache-2.0" +edition = "2018" + +[dependencies] +byteorder = { version = "1", default-features = false } +cortex-m = "^0.6.0" +cortex-m-rt = "*" +cortex-m-rt-macros = "*" +panic-abort = "0.3.2" +rtt-target = { version = "*", features = ["cortex-m"] } + +[profile.dev] +panic = "abort" +lto = true +opt-level = 3 + +[profile.release] +panic = "abort" +lto = true +# Level "z" may decrease the binary size more of necessary. +opt-level = 3 diff --git a/bootloader/README.md b/bootloader/README.md new file mode 100644 index 0000000..ae4f510 --- /dev/null +++ b/bootloader/README.md @@ -0,0 +1,36 @@ +# OpenSK Bootloader + +This bootloader supports upgradability for OpenSK. Its functionality is to + +- check images on A/B partitions, +- boot the most recent valid partition. + +## How to use + +The bootloader is built and deployed by OpenSK's `deploy.py`. If your board +defines a metadata address, it is detected as an upgradable board and this +bootloader is flashed to memory address 0. + +## How to debug + +The bootloader prints debug message over RTT when compiled in debug mode. Using +`nrfjprog` for flashing and inspecting memory is recommended for debugging. + +```shell +RUSTFLAGS="-C link-arg=-Wl,-Tlink.x -C link-arg=-nostartfiles" \ + cargo build --target thumbv7em-none-eabi +llvm-objcopy -O ihex target/thumbv7em-none-eabi/debug/bootloader \ + target/thumbv7em-none-eabi/debug/bootloader.hex +nrfjprog --program target/thumbv7em-none-eabi/debug/bootloader.hex \ + --sectorerase -f nrf52 --reset +``` + +To read the debug messages, open two terminals for: + +```shell +JLinkRTTLogger -device NRF52840_XXAA -if swd -speed 1000 -RTTchannel 0 +JLinkRTTClient +``` + +The first command also logs the output to a file. The second shows all output in +real time. diff --git a/bootloader/build.rs b/bootloader/build.rs new file mode 100644 index 0000000..128eba6 --- /dev/null +++ b/bootloader/build.rs @@ -0,0 +1,17 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +fn main() { + println!("cargo:rerun-if-changed=memory.x"); +} diff --git a/bootloader/memory.x b/bootloader/memory.x new file mode 100644 index 0000000..82f87af --- /dev/null +++ b/bootloader/memory.x @@ -0,0 +1,21 @@ +/* Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +MEMORY +{ + FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 16K + RAM (rwx) : ORIGIN = 0x20020000, LENGTH = 128K +} + diff --git a/bootloader/rust-toolchain b/bootloader/rust-toolchain new file mode 100644 index 0000000..645c2cb --- /dev/null +++ b/bootloader/rust-toolchain @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly-2021-03-25" +components = ["clippy", "rustfmt"] +targets = ["thumbv7em-none-eabi"] diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs new file mode 100644 index 0000000..51bd03d --- /dev/null +++ b/bootloader/src/main.rs @@ -0,0 +1,149 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![no_main] +#![no_std] + +extern crate cortex_m; +extern crate cortex_m_rt as rt; + +use byteorder::{ByteOrder, LittleEndian}; +use core::convert::TryInto; +use core::ptr; +use cortex_m::asm; +use panic_abort as _; +use rt::entry; +#[cfg(debug_assertions)] +use rtt_target::{rprintln, rtt_init_print}; + +/// Size of a flash page in bytes. +const PAGE_SIZE: usize = 0x1000; + +/// A flash page. +type Page = [u8; PAGE_SIZE]; + +/// Reads a page of memory. +unsafe fn read_page(address: usize) -> Page { + debug_assert!(address % PAGE_SIZE == 0); + let address_pointer = address as *const Page; + ptr::read(address_pointer) +} + +/// Parsed metadata for a firmware partition. +struct Metadata { + checksum: [u8; 32], + timestamp: u32, + address: u32, +} + +impl Metadata { + pub const DATA_LEN: usize = 40; +} + +/// Reads the metadata from a flash page. +impl From for Metadata { + fn from(page: Page) -> Self { + Metadata { + checksum: page[0..32].try_into().unwrap(), + timestamp: LittleEndian::read_u32(&page[32..36]), + address: LittleEndian::read_u32(&page[36..Metadata::DATA_LEN]), + } + } +} + +/// Location of a firmware partition's data. +struct BootPartition { + firmware_address: usize, + metadata_address: usize, +} + +impl BootPartition { + const _FIRMWARE_LENGTH: usize = 0x00040000; + + /// Reads the metadata, returns the timestamp if all checks pass. + pub fn read_timestamp(&self) -> Result { + let metadata_page = unsafe { read_page(self.metadata_address) }; + let hash_value = self.compute_upgrade_hash(&metadata_page); + let metadata = Metadata::from(metadata_page); + if self.firmware_address != metadata.address as usize { + #[cfg(debug_assertions)] + rprintln!( + "Firmware address mismatch: expected 0x{:08X}, metadata 0x{:08X}", + self.firmware_address, + metadata.address as usize + ); + return Err(()); + } + if hash_value != metadata.checksum { + #[cfg(debug_assertions)] + rprintln!("Hash mismatch"); + return Err(()); + } + Ok(metadata.timestamp) + } + + /// Placeholder for the SHA256 implementation. + /// + /// TODO implemented in next PR + /// Without it, the bootloader will never boot anything. + fn compute_upgrade_hash(&self, _metadata_page: &[u8]) -> [u8; 32] { + [0; 32] + } + + /// Jump to the firmware. + pub fn boot(&self) -> ! { + let address = self.firmware_address; + + #[cfg(debug_assertions)] + rprintln!("Boot jump to {:08X}", address); + let address_pointer = address as *const u32; + // https://docs.rs/cortex-m/0.7.2/cortex_m/asm/fn.bootload.html + unsafe { asm::bootload(address_pointer) }; + } +} + +#[entry] +fn main() -> ! { + #[cfg(debug_assertions)] + rtt_init_print!(); + #[cfg(debug_assertions)] + rprintln!("Starting bootloader"); + let partition_a = BootPartition { + firmware_address: 0x20000, + metadata_address: 0x4000, + }; + let partition_b = BootPartition { + firmware_address: 0x60000, + metadata_address: 0x5000, + }; + #[cfg(debug_assertions)] + rprintln!("Reading partition A"); + let timestamp_a = partition_a.read_timestamp(); + #[cfg(debug_assertions)] + rprintln!("Reading partition B"); + let timestamp_b = partition_b.read_timestamp(); + + match (timestamp_a, timestamp_b) { + (Ok(t1), Ok(t2)) => { + if t1 >= t2 { + partition_a.boot() + } else { + partition_b.boot() + } + } + (Ok(_), Err(_)) => partition_a.boot(), + (Err(_), Ok(_)) => partition_b.boot(), + (Err(_), Err(_)) => panic!(), + } +} diff --git a/deploy.py b/deploy.py index de264ec..cd22078 100755 --- a/deploy.py +++ b/deploy.py @@ -384,6 +384,39 @@ class OpenSKInstaller: self._check_invariants() self._build_app_or_example(is_example=False) + def build_bootloader(self): + """Builds the upgrade bootloader.""" + props = SUPPORTED_BOARDS[self.args.board] + info("Building bootloader") + rust_flags = [ + f"--remap-path-prefix={os.getcwd()}=", + "-C", + "link-arg=-Wl,-Tlink.x", + "-C", + "link-arg=-nostartfiles", + ] + env = os.environ.copy() + env["RUSTFLAGS"] = " ".join(rust_flags) + cargo_command = ["cargo", "build", "--release", f"--target={props.arch}"] + self.checked_command(cargo_command, cwd="bootloader", env=env) + binary_path = os.path.join("target", props.arch, "release", "bootloader") + objcopy_command = [ + "llvm-objcopy", "-O", "binary", binary_path, f"{binary_path}.bin" + ] + self.checked_command(objcopy_command, cwd="bootloader") + + def flash_bootloader(self): + """Flashes the upgrade bootloader.""" + props = SUPPORTED_BOARDS[self.args.board] + info("Flashing bootloader") + bin_file = os.path.join("bootloader", "target", props.arch, "release", + "bootloader.bin") + if not os.path.exists(bin_file): + fatal(f"File not found: {bin_file}") + with open(bin_file, "rb") as bootloader_bin: + bootloader = bootloader_bin.read() + self.write_binary(bootloader, 0) + def _build_app_or_example(self, is_example: bool): """Builds the application specified through args. @@ -732,9 +765,13 @@ class OpenSKInstaller: return 0 # Compile what needs to be compiled + board_props = SUPPORTED_BOARDS[self.args.board] if self.args.tockos: self.build_tockos() + if board_props.metadata_address is not None: + self.build_bootloader() + if self.args.application == "ctap2": self.generate_crypto_materials(self.args.regenerate_keys) self.build_opensk() @@ -748,7 +785,6 @@ class OpenSKInstaller: self.clear_storage() # Flashing - board_props = SUPPORTED_BOARDS[self.args.board] if self.args.programmer in ("jlink", "openocd"): # We rely on Tockloader to do the job if self.args.clear_apps: @@ -756,6 +792,9 @@ class OpenSKInstaller: if self.args.tockos: # Install Tock OS self.install_tock_os() + if board_props.metadata_address is not None: + # Install the bootloader + self.flash_bootloader() # Install padding and application if needed if self.args.application: self.install_padding() diff --git a/docs/boards/nrf52840dk.md b/docs/boards/nrf52840dk.md index 5be25b3..6976442 100644 --- a/docs/boards/nrf52840dk.md +++ b/docs/boards/nrf52840dk.md @@ -48,3 +48,25 @@ There are 3 switches that need to be in the correct position: * Power (bottom left): On * nRF power source (center left): VDD * SW6 (top right): DEFAULT + +### Upgradability + +There are variants of the board that introduce A/B partitions for upgrading the +firmware. You can bootstrap an upgradable board using one of the two commands: + +```shell +./deploy.py --board=nrf52840dk_opensk_a --opensk +./deploy.py --board=nrf52840dk_opensk_b --opensk +``` + +Afterwards, you can upgrade the other partition with + +```shell +./tools/perform_upgrade.sh nrf52840dk_opensk_b +./tools/perform_upgrade.sh nrf52840dk_opensk_a +``` + +respectively. You can only upgrade the partition that is not currently running, +so always alternate your calls to `perform_upgrade.sh`. Otherwise, this script +works like `deploy.py`. You can call it even after you locked down your device, +to deploy changes to your development board. diff --git a/docs/install.md b/docs/install.md index c7183bb..3a661bd 100644 --- a/docs/install.md +++ b/docs/install.md @@ -28,6 +28,7 @@ following: * `nrfutil` (can be installed using `pip3 install nrfutil`) if you want to flash a device with DFU * `uuid-runtime` if you are missing the `uuidgen` command. +* `llvm` if you want to use the upgradability feature. The proprietary software to use the default programmer can be found on the [Segger website](https://www.segger.com/downloads/jlink). Please follow their @@ -149,3 +150,23 @@ If your board is already flashed with Tock OS, you may skip installing it: For more options, we invite you to read the help of our `deploy.py` script by running `./deploy.py --help`. + +### Upgradability + +We experiment with a new CTAP command to allow upgrading your device without +access to its debugging port. For that purpose, the flash storage is split into +4 parts: + +* the bootloader to decide with partition to boot +* firmware partition A +* firmware partition B +* the persistent storage for credentials + +The storage is backward compatible to non-upgradable boards. Deploying an +upgradable board automatically installs the bootloader. Please keep in mind that +you have to safely store your private signing key for upgrades if you want to +use this feature. For more information on the cryptographic material, see +[Customization](customization.md). + +So far, upgradability is only supported for the development board. See the +instructions on the [board specific page](boards/nrf52840dk.md). diff --git a/run_desktop_tests.sh b/run_desktop_tests.sh index 28fa740..3c0e9f4 100755 --- a/run_desktop_tests.sh +++ b/run_desktop_tests.sh @@ -29,6 +29,9 @@ cd ../.. cd tools/heapviz cargo fmt --all -- --check cd ../.. +cd bootloader +cargo fmt --all -- --check +cd .. echo "Running Clippy lints..." cargo clippy --all-targets --features std -- -A clippy::new_without_default -D warnings @@ -55,6 +58,11 @@ echo "Checking that examples build properly..." cargo check --release --target=thumbv7em-none-eabi --examples cargo check --release --target=thumbv7em-none-eabi --examples --features with_nfc +echo "Checking that bootloader builds properly..." +cd bootloader +cargo check --release --target=thumbv7em-none-eabi +cd .. + echo "Checking that fuzz targets build properly..." cargo fuzz build cd libraries/cbor