Files
OpenSK/src/env/tock/storage.rs
kaczmarczyck ca65902a8f CTAP library move (#602)
* Moves all CTAP logic into its own library

* workflows fix test

* more coveralls workflow tests
2023-03-07 15:56:46 +01:00

573 lines
20 KiB
Rust

// Copyright 2019-2023 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.
use alloc::borrow::Cow;
use alloc::vec::Vec;
use arrayref::array_ref;
use byteorder::{ByteOrder, LittleEndian};
use core::cell::Cell;
use crypto::sha256::Sha256;
use crypto::{ecdsa, Hash256};
use libtock_core::{callback, syscalls};
use opensk::api::upgrade_storage::helper::{find_slice, is_aligned, ModRange, Partition};
use opensk::api::upgrade_storage::UpgradeStorage;
use persistent_store::{Storage, StorageError, StorageIndex, StorageResult};
const DRIVER_NUMBER: usize = 0x50003;
const METADATA_SIGN_OFFSET: usize = 0x800;
const UPGRADE_PUBLIC_KEY: &[u8; 65] =
include_bytes!(concat!(env!("OUT_DIR"), "/opensk_upgrade_pubkey.bin"));
mod subscribe_nr {
pub const DONE: usize = 0;
}
mod command_nr {
pub const GET_INFO: usize = 1;
pub mod get_info_nr {
pub const WORD_SIZE: usize = 0;
pub const PAGE_SIZE: usize = 1;
pub const MAX_WORD_WRITES: usize = 2;
pub const MAX_PAGE_ERASES: usize = 3;
}
pub const WRITE_SLICE: usize = 2;
pub const ERASE_PAGE: usize = 3;
}
mod allow_nr {
pub const WRITE_SLICE: usize = 0;
}
mod memop_nr {
pub const STORAGE_CNT: u32 = 12;
pub const STORAGE_PTR: u32 = 13;
pub const STORAGE_LEN: u32 = 14;
pub const STORAGE_TYPE: u32 = 15;
}
mod storage_type {
pub const STORE: usize = 1;
pub const PARTITION: usize = 2;
}
fn get_info(nr: usize, arg: usize) -> StorageResult<usize> {
let code = syscalls::command(DRIVER_NUMBER, command_nr::GET_INFO, nr, arg);
code.map_err(|_| StorageError::CustomError)
}
fn memop(nr: u32, arg: usize) -> StorageResult<usize> {
let code = unsafe { syscalls::raw::memop(nr, arg) };
if code < 0 {
Err(StorageError::CustomError)
} else {
Ok(code as usize)
}
}
fn block_command(driver: usize, cmd: usize, arg1: usize, arg2: usize) -> StorageResult<()> {
let done = Cell::new(None);
let mut alarm = |status| done.set(Some(status));
let subscription = syscalls::subscribe::<callback::Identity1Consumer, _>(
DRIVER_NUMBER,
subscribe_nr::DONE,
&mut alarm,
);
if subscription.is_err() {
return Err(StorageError::CustomError);
}
let code = syscalls::command(driver, cmd, arg1, arg2);
if code.is_err() {
return Err(StorageError::CustomError);
}
libtock_drivers::util::yieldk_for(|| done.get().is_some());
if done.get().unwrap() == 0 {
Ok(())
} else {
Err(StorageError::CustomError)
}
}
unsafe fn read_slice(address: usize, length: usize) -> &'static [u8] {
core::slice::from_raw_parts(address as *const u8, length)
}
fn write_slice(ptr: usize, value: &[u8]) -> StorageResult<()> {
let code = unsafe {
syscalls::raw::allow(
DRIVER_NUMBER,
allow_nr::WRITE_SLICE,
// We rely on the driver not writing to the slice. This should use read-only allow
// when available. See https://github.com/tock/tock/issues/1274.
value.as_ptr() as *mut u8,
value.len(),
)
};
if code < 0 {
return Err(StorageError::CustomError);
}
block_command(DRIVER_NUMBER, command_nr::WRITE_SLICE, ptr, value.len())
}
fn erase_page(ptr: usize, page_length: usize) -> StorageResult<()> {
block_command(DRIVER_NUMBER, command_nr::ERASE_PAGE, ptr, page_length)
}
pub struct TockStorage {
word_size: usize,
page_size: usize,
num_pages: usize,
max_word_writes: usize,
max_page_erases: usize,
storage_locations: Vec<&'static [u8]>,
}
impl TockStorage {
/// Provides access to the embedded flash if available.
///
/// # Errors
///
/// Returns `CustomError` if any of the following conditions do not hold:
/// - The word size is a power of two.
/// - The page size is a power of two.
/// - The page size is a multiple of the word size.
/// - The storage is page-aligned.
pub fn new() -> StorageResult<TockStorage> {
let mut syscall = TockStorage {
word_size: get_info(command_nr::get_info_nr::WORD_SIZE, 0)?,
page_size: get_info(command_nr::get_info_nr::PAGE_SIZE, 0)?,
num_pages: 0,
max_word_writes: get_info(command_nr::get_info_nr::MAX_WORD_WRITES, 0)?,
max_page_erases: get_info(command_nr::get_info_nr::MAX_PAGE_ERASES, 0)?,
storage_locations: Vec::new(),
};
if !syscall.word_size.is_power_of_two()
|| !syscall.page_size.is_power_of_two()
|| !syscall.is_word_aligned(syscall.page_size)
{
return Err(StorageError::CustomError);
}
for i in 0..memop(memop_nr::STORAGE_CNT, 0)? {
if memop(memop_nr::STORAGE_TYPE, i)? != storage_type::STORE {
continue;
}
let storage_ptr = memop(memop_nr::STORAGE_PTR, i)?;
let storage_len = memop(memop_nr::STORAGE_LEN, i)?;
if !syscall.is_page_aligned(storage_ptr) || !syscall.is_page_aligned(storage_len) {
return Err(StorageError::CustomError);
}
syscall.num_pages += storage_len / syscall.page_size;
syscall
.storage_locations
.push(unsafe { core::slice::from_raw_parts(storage_ptr as *mut u8, storage_len) });
}
Ok(syscall)
}
fn is_word_aligned(&self, x: usize) -> bool {
is_aligned(self.word_size, x)
}
fn is_page_aligned(&self, x: usize) -> bool {
is_aligned(self.page_size, x)
}
}
impl Storage for TockStorage {
fn word_size(&self) -> usize {
self.word_size
}
fn page_size(&self) -> usize {
self.page_size
}
fn num_pages(&self) -> usize {
self.num_pages
}
fn max_word_writes(&self) -> usize {
self.max_word_writes
}
fn max_page_erases(&self) -> usize {
self.max_page_erases
}
fn read_slice(&self, index: StorageIndex, length: usize) -> StorageResult<Cow<[u8]>> {
let start = index.range(length, self)?.start;
find_slice(&self.storage_locations, start, length).map(Cow::Borrowed)
}
fn write_slice(&mut self, index: StorageIndex, value: &[u8]) -> StorageResult<()> {
if !self.is_word_aligned(index.byte) || !self.is_word_aligned(value.len()) {
return Err(StorageError::NotAligned);
}
let ptr = self.read_slice(index, value.len())?.as_ptr() as usize;
write_slice(ptr, value)
}
fn erase_page(&mut self, page: usize) -> StorageResult<()> {
let index = StorageIndex { page, byte: 0 };
let length = self.page_size();
let ptr = self.read_slice(index, length)?.as_ptr() as usize;
erase_page(ptr, length)
}
}
pub struct TockUpgradeStorage {
page_size: usize,
partition: Partition,
metadata: ModRange,
running_metadata: ModRange,
identifier: u32,
}
impl TockUpgradeStorage {
// Ideally, the kernel should tell us metadata and partitions directly.
// This code only works for one layout, refactor this into the storage driver to support more.
const METADATA_ADDRESS: usize = 0x4000;
const PARTITION_ADDRESS_A: usize = 0x20000;
const PARTITION_ADDRESS_B: usize = 0x60000;
/// Provides access to the other upgrade partition and metadata if available.
///
/// The implementation assumes that storage locations returned by the kernel through
/// `memop_nr::STORAGE_*` calls are in address space order.
///
/// # Errors
///
/// Returns `CustomError` if any of the following conditions do not hold:
/// - The page size is a power of two.
/// - The storage slices are page-aligned.
/// - There are no partition or no metadata slices.
/// Returns a `NotAligned` error if partitions or metadata ranges are
/// - not exclusive or,
/// - not consecutive.
pub fn new() -> StorageResult<TockUpgradeStorage> {
let mut locations = TockUpgradeStorage {
page_size: get_info(command_nr::get_info_nr::PAGE_SIZE, 0)?,
partition: Partition::new(),
metadata: ModRange::new_empty(),
running_metadata: ModRange::new_empty(),
identifier: Self::PARTITION_ADDRESS_A as u32,
};
if !locations.page_size.is_power_of_two() {
return Err(StorageError::CustomError);
}
let mut firmware_range = ModRange::new_empty();
for i in 0..memop(memop_nr::STORAGE_CNT, 0)? {
let storage_type = memop(memop_nr::STORAGE_TYPE, i)?;
if !matches!(storage_type, storage_type::PARTITION) {
continue;
};
let storage_ptr = memop(memop_nr::STORAGE_PTR, i)?;
let storage_len = memop(memop_nr::STORAGE_LEN, i)?;
if !locations.is_page_aligned(storage_ptr) || !locations.is_page_aligned(storage_len) {
return Err(StorageError::CustomError);
}
let range = ModRange::new(storage_ptr, storage_len);
match range.start() {
Self::METADATA_ADDRESS => {
// Will be swapped if we are on B.
locations.metadata = ModRange::new(range.start(), locations.page_size);
locations.running_metadata =
ModRange::new(range.start() + locations.page_size, locations.page_size);
}
_ => {
if !firmware_range.append(&range) {
return Err(StorageError::NotAligned);
}
}
}
}
if firmware_range.is_empty()
|| locations.metadata.is_empty()
|| locations.running_metadata.is_empty()
{
return Err(StorageError::CustomError);
}
if firmware_range.start() == Self::PARTITION_ADDRESS_B {
core::mem::swap(&mut locations.metadata, &mut locations.running_metadata);
locations.identifier = Self::PARTITION_ADDRESS_B as u32;
}
if !locations.partition.append(locations.metadata.clone()) {
return Err(StorageError::NotAligned);
}
if !locations.partition.append(firmware_range) {
return Err(StorageError::NotAligned);
}
Ok(locations)
}
fn is_page_aligned(&self, x: usize) -> bool {
is_aligned(self.page_size, x)
}
/// Returns whether the metadata is contained in this range or not.
///
/// Assumes that metadata is written in one call per range. If the metadata is only partially
/// contained, returns an error.
fn contains_metadata(&self, checked_range: &ModRange) -> StorageResult<bool> {
if checked_range.intersects_range(&self.metadata) {
if checked_range.contains_range(&self.metadata) {
Ok(true)
} else {
Err(StorageError::NotAligned)
}
} else {
Ok(false)
}
}
/// Checks if the metadata's hash matches the partition's content.
fn check_partition_hash(&self, metadata: &[u8]) -> StorageResult<()> {
let start_address = self.metadata.start() + METADATA_SIGN_OFFSET;
let mut hasher = Sha256::new();
for range in self.partition.ranges_from(start_address) {
let partition_slice = unsafe { read_slice(range.start(), range.length()) };
// The hash implementation handles this in chunks, so no memory issues.
hasher.update(partition_slice);
}
let computed_hash = hasher.finalize();
if &computed_hash != parse_metadata_hash(metadata) {
return Err(StorageError::CustomError);
}
Ok(())
}
}
impl UpgradeStorage for TockUpgradeStorage {
fn write_bundle(&mut self, offset: usize, data: Vec<u8>) -> StorageResult<()> {
if data.is_empty() {
return Err(StorageError::OutOfBounds);
}
let address = self
.partition
.find_address(offset, data.len())
.ok_or(StorageError::OutOfBounds)?;
let write_range = ModRange::new(address, data.len());
if self.contains_metadata(&write_range)? {
let new_metadata = &data[self.metadata.start() - address..][..self.metadata.length()];
check_metadata(self, UPGRADE_PUBLIC_KEY, new_metadata)?;
}
// Erases all pages that have their first byte in the write range.
// Since we expect calls in order, we don't want to erase half-written pages.
for address in write_range.aligned_iter(self.page_size) {
erase_page(address, self.page_size)?;
}
write_slice(address, &data)?;
let written_slice = unsafe { read_slice(address, data.len()) };
if written_slice != data {
return Err(StorageError::CustomError);
}
// Case: Last slice is written.
if data.len() == self.partition.length() - offset {
let metadata = unsafe { read_slice(self.metadata.start(), self.metadata.length()) };
self.check_partition_hash(&metadata)?;
}
Ok(())
}
fn bundle_identifier(&self) -> u32 {
self.identifier
}
fn running_firmware_version(&self) -> u64 {
let running_metadata = unsafe {
read_slice(
self.running_metadata.start(),
self.running_metadata.length(),
)
};
parse_metadata_version(running_metadata)
}
}
/// Parses the metadata of an upgrade, and checks its correctness.
///
/// The metadata is a page starting with:
/// - 32 B upgrade hash (SHA256)
/// - 64 B signature,
/// that are not signed over. The second part is included in the signature with
/// - 8 B version and
/// - 4 B partition address in little endian encoding
/// written at METADATA_SIGN_OFFSET.
///
/// Checks signature correctness against the hash, and whether the partition offset matches.
/// Whether the hash matches the partition content is not tested here!
fn check_metadata(
upgrade_locations: &impl UpgradeStorage,
public_key_bytes: &[u8],
metadata: &[u8],
) -> StorageResult<()> {
const METADATA_LEN: usize = 0x1000;
if metadata.len() != METADATA_LEN {
return Err(StorageError::CustomError);
}
let version = parse_metadata_version(metadata);
if version < upgrade_locations.running_firmware_version() {
return Err(StorageError::CustomError);
}
let metadata_address = LittleEndian::read_u32(&metadata[METADATA_SIGN_OFFSET + 8..][..4]);
if metadata_address != upgrade_locations.bundle_identifier() {
return Err(StorageError::CustomError);
}
verify_signature(
array_ref!(metadata, 32, 64),
public_key_bytes,
parse_metadata_hash(metadata),
)?;
Ok(())
}
/// Parses the metadata, returns the hash.
fn parse_metadata_hash(data: &[u8]) -> &[u8; 32] {
array_ref!(data, 0, 32)
}
/// Parses the metadata, returns the firmware version.
fn parse_metadata_version(data: &[u8]) -> u64 {
LittleEndian::read_u64(&data[METADATA_SIGN_OFFSET..][..8])
}
/// Verifies the signature over the given hash.
///
/// The public key is COSE encoded, and the hash is a SHA256.
fn verify_signature(
signature_bytes: &[u8; 64],
public_key_bytes: &[u8],
signed_hash: &[u8; 32],
) -> StorageResult<()> {
let signature =
ecdsa::Signature::from_bytes(signature_bytes).ok_or(StorageError::CustomError)?;
let public_key = ecdsa::PubKey::from_bytes_uncompressed(public_key_bytes)
.ok_or(StorageError::CustomError)?;
if !public_key.verify_hash_vartime(signed_hash, &signature) {
return Err(StorageError::CustomError);
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use opensk::env::test::TestEnv;
use opensk::env::Env;
#[test]
fn test_check_metadata() {
let mut env = TestEnv::default();
let private_key = crypto::ecdsa::SecKey::gensk(env.rng());
let upgrade_locations = env.upgrade_storage().unwrap();
const METADATA_LEN: usize = 0x1000;
const METADATA_SIGN_OFFSET: usize = 0x800;
let mut metadata = vec![0xFF; METADATA_LEN];
LittleEndian::write_u32(&mut metadata[METADATA_SIGN_OFFSET + 8..][..4], 0x60000);
let mut signed_over_data = metadata[METADATA_SIGN_OFFSET..].to_vec();
signed_over_data.extend(&[0xFF; 0x20000]);
let signed_hash = Sha256::hash(&signed_over_data);
metadata[..32].copy_from_slice(&signed_hash);
let signature = private_key.sign_rfc6979::<Sha256>(&signed_over_data);
let mut signature_bytes = [0; ecdsa::Signature::BYTES_LENGTH];
signature.to_bytes(&mut signature_bytes);
metadata[32..96].copy_from_slice(&signature_bytes);
let public_key = private_key.genpk();
let mut public_key_bytes = [0; 65];
public_key.to_bytes_uncompressed(&mut public_key_bytes);
assert_eq!(
check_metadata(upgrade_locations, &public_key_bytes, &metadata),
Ok(())
);
// Manipulating the partition address fails.
metadata[METADATA_SIGN_OFFSET + 8] = 0x88;
assert_eq!(
check_metadata(upgrade_locations, &public_key_bytes, &metadata),
Err(StorageError::CustomError)
);
metadata[METADATA_SIGN_OFFSET + 8] = 0x00;
// Wrong metadata length fails.
assert_eq!(
check_metadata(
upgrade_locations,
&public_key_bytes,
&metadata[..METADATA_LEN - 1]
),
Err(StorageError::CustomError)
);
// Manipulating the hash fails.
metadata[0] ^= 0x01;
assert_eq!(
check_metadata(upgrade_locations, &public_key_bytes, &metadata),
Err(StorageError::CustomError)
);
metadata[0] ^= 0x01;
// Manipulating the signature fails.
metadata[32] ^= 0x01;
assert_eq!(
check_metadata(upgrade_locations, &public_key_bytes, &metadata),
Err(StorageError::CustomError)
);
}
#[test]
fn test_verify_signature() {
let mut env = TestEnv::default();
let private_key = crypto::ecdsa::SecKey::gensk(env.rng());
let message = [0x44; 64];
let signed_hash = Sha256::hash(&message);
let signature = private_key.sign_rfc6979::<Sha256>(&message);
let mut signature_bytes = [0; ecdsa::Signature::BYTES_LENGTH];
signature.to_bytes(&mut signature_bytes);
let public_key = private_key.genpk();
let mut public_key_bytes = [0; 65];
public_key.to_bytes_uncompressed(&mut public_key_bytes);
assert_eq!(
verify_signature(&signature_bytes, &public_key_bytes, &signed_hash),
Ok(())
);
assert_eq!(
verify_signature(&signature_bytes, &public_key_bytes, &[0x55; 32]),
Err(StorageError::CustomError)
);
public_key_bytes[0] ^= 0x01;
assert_eq!(
verify_signature(&signature_bytes, &public_key_bytes, &signed_hash),
Err(StorageError::CustomError)
);
public_key_bytes[0] ^= 0x01;
signature_bytes[0] ^= 0x01;
assert_eq!(
verify_signature(&signature_bytes, &public_key_bytes, &signed_hash),
Err(StorageError::CustomError)
);
}
}