Bootloader infrastructure and main logic (#404)

* adds bootloader code without a SHA256 implementation

* small fixes and typos
This commit is contained in:
kaczmarczyck
2022-03-03 22:01:42 +01:00
committed by GitHub
parent c401216544
commit 187111f9c5
13 changed files with 360 additions and 1 deletions

View File

@@ -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

View File

@@ -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

2
bootloader/.cargo/config Normal file
View File

@@ -0,0 +1,2 @@
[target.thumbv7em-none-eabi]
linker = "arm-none-eabi-gcc"

28
bootloader/Cargo.toml Normal file
View File

@@ -0,0 +1,28 @@
[package]
name = "bootloader"
version = "0.1.0"
authors = [
"Fabian Kaczmarczyck <kaczmarczyck@google.com>",
]
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

36
bootloader/README.md Normal file
View File

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

17
bootloader/build.rs Normal file
View File

@@ -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");
}

21
bootloader/memory.x Normal file
View File

@@ -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
}

View File

@@ -0,0 +1,4 @@
[toolchain]
channel = "nightly-2021-03-25"
components = ["clippy", "rustfmt"]
targets = ["thumbv7em-none-eabi"]

149
bootloader/src/main.rs Normal file
View File

@@ -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<Page> 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<u32, ()> {
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!(),
}
}

View File

@@ -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()

View File

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

View File

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

View File

@@ -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