Moves vendor commands into TockEnv (#614)

This move changes the Env trait: It removes all functionality that is
used only in vendor commands (`FirmwareProtection`, `UpgradeStorage`)
and adds a function to call when parsing CBOR commands.

The abstraction necessary to test these commands is instead realized
through compile flags. The mock upgrade storage is active when compiled
for std for example.
This commit is contained in:
kaczmarczyck
2023-04-17 00:17:37 +02:00
committed by GitHub
parent a1d6ed0223
commit 3091b5a29d
23 changed files with 1164 additions and 1047 deletions

114
src/env/tock/buffer_upgrade_storage.rs vendored Normal file
View File

@@ -0,0 +1,114 @@
// Copyright 2021-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 super::storage_helper::ModRange;
use alloc::boxed::Box;
use persistent_store::{StorageError, StorageResult};
const PARTITION_LENGTH: usize = 0x41000;
const METADATA_LENGTH: usize = 0x1000;
pub struct BufferUpgradeStorage {
/// Content of the partition storage.
partition: Box<[u8]>,
}
impl BufferUpgradeStorage {
pub fn new() -> StorageResult<BufferUpgradeStorage> {
Ok(BufferUpgradeStorage {
partition: vec![0xff; PARTITION_LENGTH].into_boxed_slice(),
})
}
#[cfg(test)]
fn read_partition(&self, offset: usize, length: usize) -> StorageResult<&[u8]> {
if length == 0 {
return Err(StorageError::OutOfBounds);
}
let partition_range = ModRange::new(0, self.partition.len());
if partition_range.contains_range(&ModRange::new(offset, length)) {
Ok(&self.partition[offset..][..length])
} else {
Err(StorageError::OutOfBounds)
}
}
pub fn write_bundle(&mut self, offset: usize, data: Vec<u8>) -> StorageResult<()> {
if offset == 0 && data.len() != METADATA_LENGTH {
return Err(StorageError::OutOfBounds);
}
if data.is_empty() {
return Err(StorageError::OutOfBounds);
}
let partition_range = ModRange::new(0, self.partition.len());
if partition_range.contains_range(&ModRange::new(offset, data.len())) {
self.partition[offset..][..data.len()].copy_from_slice(&data);
Ok(())
} else {
Err(StorageError::OutOfBounds)
}
}
pub fn bundle_identifier(&self) -> u32 {
0x60000
}
pub fn running_firmware_version(&self) -> u64 {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_write_bundle() {
let mut storage = BufferUpgradeStorage::new().unwrap();
assert_eq!(storage.read_partition(0, 2).unwrap(), &[0xFF, 0xFF]);
assert!(storage.write_bundle(1, vec![0x88, 0x88]).is_ok());
assert_eq!(storage.read_partition(0, 2).unwrap(), &[0xFF, 0x88]);
assert_eq!(
storage.write_bundle(PARTITION_LENGTH - 1, vec![0x88, 0x88],),
Err(StorageError::OutOfBounds)
);
assert_eq!(
storage.read_partition(PARTITION_LENGTH - 2, 2).unwrap(),
&[0xFF, 0xFF]
);
assert_eq!(
storage.read_partition(PARTITION_LENGTH - 1, 2),
Err(StorageError::OutOfBounds)
);
assert_eq!(
storage.write_bundle(4, vec![]),
Err(StorageError::OutOfBounds)
);
assert_eq!(
storage.write_bundle(PARTITION_LENGTH + 4, vec![]),
Err(StorageError::OutOfBounds)
);
assert_eq!(storage.read_partition(4, 0), Err(StorageError::OutOfBounds));
assert_eq!(
storage.read_partition(PARTITION_LENGTH + 4, 0),
Err(StorageError::OutOfBounds)
);
}
#[test]
fn partition_slice() {
let storage = BufferUpgradeStorage::new().unwrap();
assert_eq!(storage.bundle_identifier(), 0x60000);
}
}

694
src/env/tock/commands.rs vendored Normal file
View File

@@ -0,0 +1,694 @@
// 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 super::TockEnv;
use alloc::vec;
use alloc::vec::Vec;
use arrayref::array_ref;
use core::convert::TryFrom;
use opensk::api::attestation_store::{self, Attestation, AttestationStore};
use opensk::api::crypto::sha256::Sha256;
use opensk::api::crypto::EC_FIELD_SIZE;
#[cfg(not(feature = "with_ctap1"))]
use opensk::api::customization::Customization;
#[cfg(not(feature = "std"))]
use opensk::ctap::check_user_presence;
use opensk::ctap::data_formats::{
extract_bool, extract_byte_string, extract_map, extract_unsigned, ok_or_missing,
};
use opensk::ctap::status_code::Ctap2StatusCode;
use opensk::ctap::{cbor_read, cbor_write, Channel};
use opensk::env::{Env, Sha};
use sk_cbor as cbor;
use sk_cbor::{cbor_map_options, destructure_cbor_map};
const VENDOR_COMMAND_CONFIGURE: u8 = 0x40;
const VENDOR_COMMAND_UPGRADE: u8 = 0x42;
const VENDOR_COMMAND_UPGRADE_INFO: u8 = 0x43;
pub fn process_vendor_command(
env: &mut TockEnv,
bytes: &[u8],
channel: Channel,
) -> Option<Vec<u8>> {
#[cfg(feature = "vendor_hid")]
if matches!(channel, Channel::VendorHid(_)) {
return None;
}
process_cbor(env, bytes, channel).unwrap_or_else(|e| Some(vec![e as u8]))
}
fn process_cbor(
env: &mut TockEnv,
bytes: &[u8],
channel: Channel,
) -> Result<Option<Vec<u8>>, Ctap2StatusCode> {
match bytes[0] {
VENDOR_COMMAND_CONFIGURE => {
let decoded_cbor = cbor_read(&bytes[1..])?;
let params = VendorConfigureParameters::try_from(decoded_cbor)?;
let response = process_vendor_configure(env, params, channel)?;
Ok(Some(encode_cbor(response.into())))
}
VENDOR_COMMAND_UPGRADE => {
let decoded_cbor = cbor_read(&bytes[1..])?;
let params = VendorUpgradeParameters::try_from(decoded_cbor)?;
process_vendor_upgrade(env, params)?;
Ok(Some(vec![Ctap2StatusCode::CTAP2_OK as u8]))
}
VENDOR_COMMAND_UPGRADE_INFO => {
let response = process_vendor_upgrade_info(env)?;
Ok(Some(encode_cbor(response.into())))
}
_ => Ok(None),
}
}
fn encode_cbor(value: cbor::Value) -> Vec<u8> {
let mut response_vec = vec![Ctap2StatusCode::CTAP2_OK as u8];
if cbor_write(value, &mut response_vec).is_err() {
vec![Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8]
} else {
response_vec
}
}
fn process_vendor_configure(
env: &mut TockEnv,
params: VendorConfigureParameters,
// Unused in std only
_channel: Channel,
) -> Result<VendorConfigureResponse, Ctap2StatusCode> {
if params.attestation_material.is_some() || params.lockdown {
// This is removed in std so we don't need too many mocks in TockEnv.
#[cfg(not(feature = "std"))]
check_user_presence(env, _channel)?;
}
// This command is for U2F support and we use the batch attestation there.
let attestation_id = attestation_store::Id::Batch;
// Sanity checks
let current_attestation = env.attestation_store().get(&attestation_id)?;
let response = match params.attestation_material {
None => VendorConfigureResponse {
cert_programmed: current_attestation.is_some(),
pkey_programmed: current_attestation.is_some(),
},
Some(data) => {
// We don't overwrite the attestation if it's already set. We don't return any error
// to not leak information.
if current_attestation.is_none() {
let attestation = Attestation {
private_key: data.private_key,
certificate: data.certificate,
};
env.attestation_store()
.set(&attestation_id, Some(&attestation))?;
}
VendorConfigureResponse {
cert_programmed: true,
pkey_programmed: true,
}
}
};
if params.lockdown {
// To avoid bricking the authenticator, we only allow lockdown
// to happen if both values are programmed or if both U2F/CTAP1 and
// batch attestation are disabled.
#[cfg(feature = "with_ctap1")]
let need_certificate = true;
#[cfg(not(feature = "with_ctap1"))]
let need_certificate = env.customization().use_batch_attestation();
if (need_certificate && !(response.pkey_programmed && response.cert_programmed))
|| !env.lock_firmware_protection()
{
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
}
Ok(response)
}
fn process_vendor_upgrade(
env: &mut TockEnv,
params: VendorUpgradeParameters,
) -> Result<(), Ctap2StatusCode> {
let VendorUpgradeParameters { offset, data, hash } = params;
let calculated_hash = Sha::<TockEnv>::digest(&data);
if hash != calculated_hash {
return Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE);
}
env.upgrade_storage()
.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND)?
.write_bundle(offset, data)
.map_err(|_| Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
}
fn process_vendor_upgrade_info(
env: &mut TockEnv,
) -> Result<VendorUpgradeInfoResponse, Ctap2StatusCode> {
let upgrade_locations = env
.upgrade_storage()
.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND)?;
Ok(VendorUpgradeInfoResponse {
info: upgrade_locations.bundle_identifier(),
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AttestationMaterial {
pub certificate: Vec<u8>,
pub private_key: [u8; EC_FIELD_SIZE],
}
impl TryFrom<cbor::Value> for AttestationMaterial {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => certificate,
0x02 => private_key,
} = extract_map(cbor_value)?;
}
let certificate = extract_byte_string(ok_or_missing(certificate)?)?;
let private_key = extract_byte_string(ok_or_missing(private_key)?)?;
if private_key.len() != EC_FIELD_SIZE {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
let private_key = array_ref!(private_key, 0, EC_FIELD_SIZE);
Ok(AttestationMaterial {
certificate,
private_key: *private_key,
})
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct VendorConfigureParameters {
pub lockdown: bool,
pub attestation_material: Option<AttestationMaterial>,
}
impl TryFrom<cbor::Value> for VendorConfigureParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => lockdown,
0x02 => attestation_material,
} = extract_map(cbor_value)?;
}
let lockdown = lockdown.map_or(Ok(false), extract_bool)?;
let attestation_material = attestation_material
.map(AttestationMaterial::try_from)
.transpose()?;
Ok(VendorConfigureParameters {
lockdown,
attestation_material,
})
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct VendorUpgradeParameters {
pub offset: usize,
pub data: Vec<u8>,
pub hash: [u8; 32],
}
impl TryFrom<cbor::Value> for VendorUpgradeParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => offset,
0x02 => data,
0x03 => hash,
} = extract_map(cbor_value)?;
}
let offset = extract_unsigned(ok_or_missing(offset)?)? as usize;
let data = extract_byte_string(ok_or_missing(data)?)?;
let hash = <[u8; 32]>::try_from(extract_byte_string(ok_or_missing(hash)?)?)
.map_err(|_| Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?;
Ok(VendorUpgradeParameters { offset, data, hash })
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct VendorConfigureResponse {
pub cert_programmed: bool,
pub pkey_programmed: bool,
}
impl From<VendorConfigureResponse> for cbor::Value {
fn from(vendor_response: VendorConfigureResponse) -> Self {
let VendorConfigureResponse {
cert_programmed,
pkey_programmed,
} = vendor_response;
cbor_map_options! {
0x01 => cert_programmed,
0x02 => pkey_programmed,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct VendorUpgradeInfoResponse {
pub info: u32,
}
impl From<VendorUpgradeInfoResponse> for cbor::Value {
fn from(vendor_upgrade_info_response: VendorUpgradeInfoResponse) -> Self {
let VendorUpgradeInfoResponse { info } = vendor_upgrade_info_response;
cbor_map_options! {
0x01 => info as u64,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use cbor::cbor_map;
const DUMMY_CHANNEL: Channel = Channel::MainHid([0x12, 0x34, 0x56, 0x78]);
#[test]
fn test_process_cbor_unrelated_input() {
let mut env = TockEnv::default();
let cbor_bytes = vec![0x01];
assert_eq!(process_cbor(&mut env, &cbor_bytes, DUMMY_CHANNEL), Ok(None));
}
#[test]
fn test_process_cbor_invalid_input() {
let mut env = TockEnv::default();
let cbor_bytes = vec![VENDOR_COMMAND_CONFIGURE];
assert_eq!(
process_cbor(&mut env, &cbor_bytes, DUMMY_CHANNEL),
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR)
);
}
#[test]
fn test_process_cbor_valid_input() {
let mut env = TockEnv::default();
let cbor_bytes = vec![VENDOR_COMMAND_UPGRADE_INFO];
assert!(process_cbor(&mut env, &cbor_bytes, DUMMY_CHANNEL)
.unwrap()
.is_some());
}
#[test]
fn test_vendor_configure_parameters() {
let dummy_cert = [0xddu8; 20];
let dummy_pkey = [0x41u8; EC_FIELD_SIZE];
// Attestation key is too short.
let cbor_value = cbor_map! {
0x01 => false,
0x02 => cbor_map! {
0x01 => dummy_cert,
0x02 => dummy_pkey[..EC_FIELD_SIZE - 1]
}
};
assert_eq!(
VendorConfigureParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
// Missing private key
let cbor_value = cbor_map! {
0x01 => false,
0x02 => cbor_map! {
0x01 => dummy_cert
}
};
assert_eq!(
VendorConfigureParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
);
// Missing certificate
let cbor_value = cbor_map! {
0x01 => false,
0x02 => cbor_map! {
0x02 => dummy_pkey
}
};
assert_eq!(
VendorConfigureParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
);
// Valid
let cbor_value = cbor_map! {
0x01 => false,
0x02 => cbor_map! {
0x01 => dummy_cert,
0x02 => dummy_pkey
},
};
assert_eq!(
VendorConfigureParameters::try_from(cbor_value),
Ok(VendorConfigureParameters {
lockdown: false,
attestation_material: Some(AttestationMaterial {
certificate: dummy_cert.to_vec(),
private_key: dummy_pkey
}),
})
);
}
#[test]
fn test_vendor_upgrade_parameters() {
// Missing offset
let cbor_value = cbor_map! {
0x02 => [0xFF; 0x100],
0x03 => [0x44; 32],
};
assert_eq!(
VendorUpgradeParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
);
// Missing data
let cbor_value = cbor_map! {
0x01 => 0x1000,
0x03 => [0x44; 32],
};
assert_eq!(
VendorUpgradeParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
);
// Invalid hash size
let cbor_value = cbor_map! {
0x01 => 0x1000,
0x02 => [0xFF; 0x100],
0x03 => [0x44; 33],
};
assert_eq!(
VendorUpgradeParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
// Missing hash
let cbor_value = cbor_map! {
0x01 => 0x1000,
0x02 => [0xFF; 0x100],
};
assert_eq!(
VendorUpgradeParameters::try_from(cbor_value),
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
);
// Valid
let cbor_value = cbor_map! {
0x01 => 0x1000,
0x02 => [0xFF; 0x100],
0x03 => [0x44; 32],
};
assert_eq!(
VendorUpgradeParameters::try_from(cbor_value),
Ok(VendorUpgradeParameters {
offset: 0x1000,
data: vec![0xFF; 0x100],
hash: [0x44; 32],
})
);
}
#[test]
fn test_deserialize_vendor_upgrade_info() {
let mut env = TockEnv::default();
let cbor_bytes = [VENDOR_COMMAND_UPGRADE_INFO];
assert!(process_cbor(&mut env, &cbor_bytes, DUMMY_CHANNEL)
.unwrap()
.is_some());
}
#[test]
fn test_vendor_configure() {
let mut env = TockEnv::default();
// Nothing should be configured at the beginning
let response = process_vendor_configure(
&mut env,
VendorConfigureParameters {
lockdown: false,
attestation_material: None,
},
DUMMY_CHANNEL,
);
assert_eq!(
response,
Ok(VendorConfigureResponse {
cert_programmed: false,
pkey_programmed: false,
})
);
// Inject dummy values
let dummy_key = [0x41u8; EC_FIELD_SIZE];
let dummy_cert = [0xddu8; 20];
let response = process_vendor_configure(
&mut env,
VendorConfigureParameters {
lockdown: false,
attestation_material: Some(AttestationMaterial {
certificate: dummy_cert.to_vec(),
private_key: dummy_key,
}),
},
DUMMY_CHANNEL,
);
assert_eq!(
response,
Ok(VendorConfigureResponse {
cert_programmed: true,
pkey_programmed: true,
})
);
assert_eq!(
env.attestation_store().get(&attestation_store::Id::Batch),
Ok(Some(Attestation {
private_key: dummy_key,
certificate: dummy_cert.to_vec(),
}))
);
// Try to inject other dummy values and check that initial values are retained.
let other_dummy_key = [0x44u8; EC_FIELD_SIZE];
let response = process_vendor_configure(
&mut env,
VendorConfigureParameters {
lockdown: false,
attestation_material: Some(AttestationMaterial {
certificate: dummy_cert.to_vec(),
private_key: other_dummy_key,
}),
},
DUMMY_CHANNEL,
);
assert_eq!(
response,
Ok(VendorConfigureResponse {
cert_programmed: true,
pkey_programmed: true,
})
);
assert_eq!(
env.attestation_store().get(&attestation_store::Id::Batch),
Ok(Some(Attestation {
private_key: dummy_key,
certificate: dummy_cert.to_vec(),
}))
);
// Now try to lock the device
let response = process_vendor_configure(
&mut env,
VendorConfigureParameters {
lockdown: true,
attestation_material: None,
},
DUMMY_CHANNEL,
);
assert_eq!(
response,
Ok(VendorConfigureResponse {
cert_programmed: true,
pkey_programmed: true,
})
);
}
#[test]
fn test_vendor_upgrade() {
// The test partition storage has size 0x40000.
// The test metadata storage has size 0x1000.
// The test identifier matches partition B.
let mut env = TockEnv::default();
const METADATA_LEN: usize = 0x1000;
let metadata = vec![0xFF; METADATA_LEN];
let metadata_hash = Sha::<TockEnv>::digest(&metadata);
let data = vec![0xFF; 0x1000];
let hash = Sha::<TockEnv>::digest(&data);
// Write to partition.
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: 0x20000,
data: data.clone(),
hash,
},
);
assert_eq!(response, Ok(()));
// TockEnv doesn't check the metadata, test its parser in your Env.
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: 0,
data: metadata.clone(),
hash: metadata_hash,
},
);
assert_eq!(response, Ok(()));
// TockEnv doesn't check the metadata, test its parser in your Env.
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: METADATA_LEN,
data: data.clone(),
hash,
},
);
assert_eq!(response, Ok(()));
// Write metadata of a wrong size.
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: 0,
data: metadata[..METADATA_LEN - 1].to_vec(),
hash: metadata_hash,
},
);
assert_eq!(response, Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE));
// Write outside of the partition.
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: 0x41000,
data: data.clone(),
hash,
},
);
assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER));
// Write a bad hash.
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: 0x20000,
data,
hash: [0xEE; 32],
},
);
assert_eq!(response, Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE));
}
#[test]
fn test_vendor_upgrade_no_second_partition() {
let mut env = TockEnv::default();
env.disable_upgrade_storage();
let data = vec![0xFF; 0x1000];
let hash = Sha::<TockEnv>::digest(&data);
let response = process_vendor_upgrade(
&mut env,
VendorUpgradeParameters {
offset: 0,
data,
hash,
},
);
assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND));
}
#[test]
fn test_vendor_upgrade_info() {
let mut env = TockEnv::default();
let bundle_identifier = env.upgrade_storage().unwrap().bundle_identifier();
let upgrade_info_reponse = process_vendor_upgrade_info(&mut env);
assert_eq!(
upgrade_info_reponse,
Ok(VendorUpgradeInfoResponse {
info: bundle_identifier,
})
);
}
#[test]
fn test_vendor_response_into_cbor() {
let response_cbor: cbor::Value = VendorConfigureResponse {
cert_programmed: true,
pkey_programmed: false,
}
.into();
assert_eq!(
response_cbor,
cbor_map_options! {
0x01 => true,
0x02 => false,
}
);
let response_cbor: cbor::Value = VendorConfigureResponse {
cert_programmed: false,
pkey_programmed: true,
}
.into();
assert_eq!(
response_cbor,
cbor_map_options! {
0x01 => false,
0x02 => true,
}
);
}
#[test]
fn test_vendor_upgrade_info_into_cbor() {
let vendor_upgrade_info_response = VendorUpgradeInfoResponse { info: 0x00060000 };
let response_cbor: cbor::Value = vendor_upgrade_info_response.into();
let expected_cbor = cbor_map! {
0x01 => 0x00060000,
};
assert_eq!(response_cbor, expected_cbor);
}
}

121
src/env/tock/mod.rs vendored
View File

@@ -12,33 +12,54 @@
// See the License for the specific language governing permissions and
// limitations under the License.
pub use self::storage::{TockStorage, TockUpgradeStorage};
use alloc::vec::Vec;
use clock::TockClock;
use core::cell::Cell;
use core::convert::TryFrom;
#[cfg(not(feature = "std"))]
use core::sync::atomic::{AtomicBool, Ordering};
use libtock_core::result::{CommandError, EALREADY};
use libtock_drivers::buttons::{self, ButtonState};
use libtock_drivers::console::Console;
#[cfg(not(feature = "std"))]
use libtock_drivers::crp;
use libtock_drivers::result::{FlexUnwrap, TockError};
use libtock_drivers::timer::Duration;
use libtock_drivers::{crp, led, rng, timer, usb_ctap_hid};
use libtock_drivers::{led, rng, timer, usb_ctap_hid};
use opensk::api::attestation_store::AttestationStore;
use opensk::api::connection::{
HidConnection, SendOrRecvError, SendOrRecvResult, SendOrRecvStatus, UsbEndpoint,
};
use opensk::api::crypto::software_crypto::SoftwareCrypto;
use opensk::api::customization::{CustomizationImpl, AAGUID_LENGTH, DEFAULT_CUSTOMIZATION};
use opensk::api::firmware_protection::FirmwareProtection;
use opensk::api::rng::Rng;
use opensk::api::user_presence::{UserPresence, UserPresenceError, UserPresenceResult};
use opensk::api::{attestation_store, key_store};
use opensk::ctap::Channel;
use opensk::env::Env;
#[cfg(feature = "std")]
use persistent_store::{BufferOptions, BufferStorage};
use persistent_store::{StorageResult, Store};
use rand_core::{impls, CryptoRng, Error, RngCore};
#[cfg(feature = "std")]
mod buffer_upgrade_storage;
mod clock;
mod commands;
#[cfg(not(feature = "std"))]
mod storage;
mod storage_helper;
mod upgrade_helper;
#[cfg(not(feature = "std"))]
pub type Storage = storage::TockStorage;
#[cfg(feature = "std")]
pub type Storage = BufferStorage;
#[cfg(not(feature = "std"))]
type UpgradeStorage = storage::TockUpgradeStorage;
#[cfg(feature = "std")]
type UpgradeStorage = buffer_upgrade_storage::BufferUpgradeStorage;
pub const AAGUID: &[u8; AAGUID_LENGTH] =
include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin"));
@@ -97,8 +118,8 @@ impl HidConnection for TockHidConnection {
pub struct TockEnv {
rng: TockRng,
store: Store<TockStorage>,
upgrade_storage: Option<TockUpgradeStorage>,
store: Store<Storage>,
upgrade_storage: Option<UpgradeStorage>,
main_connection: TockHidConnection,
#[cfg(feature = "vendor_hid")]
vendor_connection: TockHidConnection,
@@ -116,7 +137,7 @@ impl Default for TockEnv {
// We rely on `take_storage` to ensure that this function is called only once.
let storage = take_storage().unwrap();
let store = Store::new(storage).ok().unwrap();
let upgrade_storage = TockUpgradeStorage::new().ok();
let upgrade_storage = UpgradeStorage::new().ok();
TockEnv {
rng: TockRng {},
store,
@@ -134,16 +155,65 @@ impl Default for TockEnv {
}
}
impl TockEnv {
/// Returns the upgrade storage instance.
///
/// Upgrade storage is optional, so implementations may return `None`. However, implementations
/// should either always return `None` or always return `Some`.
pub fn upgrade_storage(&mut self) -> Option<&mut UpgradeStorage> {
self.upgrade_storage.as_mut()
}
pub fn disable_upgrade_storage(&mut self) {
self.upgrade_storage = None;
}
pub fn lock_firmware_protection(&mut self) -> bool {
#[cfg(not(feature = "std"))]
{
matches!(
crp::set_protection(crp::ProtectionLevel::FullyLocked),
Ok(())
| Err(TockError::Command(CommandError {
return_code: EALREADY,
..
}))
)
}
#[cfg(feature = "std")]
{
true
}
}
}
/// Returns the unique storage instance.
///
/// # Panics
///
/// - If called a second time.
pub fn take_storage() -> StorageResult<TockStorage> {
#[cfg(not(feature = "std"))]
pub fn take_storage() -> StorageResult<Storage> {
// Make sure the storage was not already taken.
static TAKEN: AtomicBool = AtomicBool::new(false);
assert!(!TAKEN.fetch_or(true, Ordering::SeqCst));
TockStorage::new()
Storage::new()
}
#[cfg(feature = "std")]
pub fn take_storage() -> StorageResult<Storage> {
// Use the Nordic configuration.
const PAGE_SIZE: usize = 0x1000;
const NUM_PAGES: usize = 20;
let store = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice();
let options = BufferOptions {
word_size: 4,
page_size: PAGE_SIZE,
max_word_writes: 2,
max_page_erases: 10000,
strict_mode: true,
};
Ok(BufferStorage::new(store, options))
}
impl UserPresence for TockEnv {
@@ -216,19 +286,6 @@ impl UserPresence for TockEnv {
}
}
impl FirmwareProtection for TockEnv {
fn lock(&mut self) -> bool {
matches!(
crp::set_protection(crp::ProtectionLevel::FullyLocked),
Ok(())
| Err(TockError::Command(CommandError {
return_code: EALREADY,
..
}))
)
}
}
impl key_store::Helper for TockEnv {}
impl AttestationStore for TockEnv {
@@ -257,12 +314,10 @@ impl AttestationStore for TockEnv {
impl Env for TockEnv {
type Rng = TockRng;
type UserPresence = Self;
type Storage = TockStorage;
type Storage = Storage;
type KeyStore = Self;
type AttestationStore = Self;
type Clock = TockClock;
type UpgradeStorage = TockUpgradeStorage;
type FirmwareProtection = Self;
type Write = Console;
type Customization = CustomizationImpl;
type HidConnection = TockHidConnection;
@@ -292,14 +347,6 @@ impl Env for TockEnv {
&mut self.clock
}
fn upgrade_storage(&mut self) -> Option<&mut Self::UpgradeStorage> {
self.upgrade_storage.as_mut()
}
fn firmware_protection(&mut self) -> &mut Self::FirmwareProtection {
self
}
fn write(&mut self) -> Self::Write {
Console::new()
}
@@ -316,6 +363,16 @@ impl Env for TockEnv {
fn vendor_hid_connection(&mut self) -> &mut Self::HidConnection {
&mut self.vendor_connection
}
fn process_vendor_command(&mut self, bytes: &[u8], channel: Channel) -> Option<Vec<u8>> {
commands::process_vendor_command(self, bytes, channel)
}
fn firmware_version(&self) -> Option<u64> {
self.upgrade_storage
.as_ref()
.map(|u| u.running_firmware_version())
}
}
pub fn blink_leds(pattern_seed: usize) {

View File

@@ -12,20 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use super::storage_helper::{find_slice, is_aligned, ModRange, Partition};
use super::upgrade_helper::{
check_metadata, parse_metadata_hash, parse_metadata_version, METADATA_SIGN_OFFSET,
};
use super::TockEnv;
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 opensk::api::crypto::sha256::Sha256;
use opensk::env::Sha;
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"));
@@ -337,7 +337,7 @@ impl TockUpgradeStorage {
/// 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();
let mut hasher = Sha::<TockEnv>::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.
@@ -349,10 +349,8 @@ impl TockUpgradeStorage {
}
Ok(())
}
}
impl UpgradeStorage for TockUpgradeStorage {
fn write_bundle(&mut self, offset: usize, data: Vec<u8>) -> StorageResult<()> {
pub fn write_bundle(&mut self, offset: usize, data: Vec<u8>) -> StorageResult<()> {
if data.is_empty() {
return Err(StorageError::OutOfBounds);
}
@@ -363,7 +361,7 @@ impl UpgradeStorage for TockUpgradeStorage {
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)?;
check_metadata::<TockEnv>(self, UPGRADE_PUBLIC_KEY, new_metadata)?;
}
// Erases all pages that have their first byte in the write range.
@@ -384,11 +382,11 @@ impl UpgradeStorage for TockUpgradeStorage {
Ok(())
}
fn bundle_identifier(&self) -> u32 {
pub fn bundle_identifier(&self) -> u32 {
self.identifier
}
fn running_firmware_version(&self) -> u64 {
pub fn running_firmware_version(&self) -> u64 {
let running_metadata = unsafe {
read_slice(
self.running_metadata.start(),
@@ -398,175 +396,3 @@ impl UpgradeStorage for TockUpgradeStorage {
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)
);
}
}

381
src/env/tock/storage_helper.rs vendored Normal file
View File

@@ -0,0 +1,381 @@
// 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.
// For compiling with std outside of tests.
#![cfg_attr(feature = "std", allow(dead_code))]
use alloc::vec::Vec;
use core::iter::Iterator;
use persistent_store::{StorageError, StorageResult};
/// Reads a slice from a list of slices.
///
/// The returned slice contains the interval `[start, start+length)`.
///
/// # Errors
///
/// Returns [`StorageError::OutOfBounds`] if the range is not within exactly one slice.
pub fn find_slice<'a>(
slices: &'a [&'a [u8]],
mut start: usize,
length: usize,
) -> StorageResult<&'a [u8]> {
for slice in slices {
if start >= slice.len() {
start -= slice.len();
continue;
}
if start + length > slice.len() {
break;
}
return Ok(&slice[start..][..length]);
}
Err(StorageError::OutOfBounds)
}
/// Checks whether the address is aligned with the block size.
///
/// Requires `block_size` to be a power of two.
pub fn is_aligned(block_size: usize, address: usize) -> bool {
debug_assert!(block_size.is_power_of_two());
address & (block_size - 1) == 0
}
/// A range implementation using start and length.
///
/// The range is treated as the interval `[start, start + length)`.
/// All objects with length of 0, regardless of the start value, are considered empty.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModRange {
start: usize,
length: usize,
}
impl ModRange {
/// Returns a new range of given start and length.
pub fn new(start: usize, length: usize) -> ModRange {
ModRange { start, length }
}
/// Create a new empty range.
pub fn new_empty() -> ModRange {
ModRange::new(0, 0)
}
/// Returns the start of the range.
pub fn start(&self) -> usize {
self.start
}
/// Returns the length of the range.
pub fn length(&self) -> usize {
self.length
}
/// Returns whether this range contains any addresses.
pub fn is_empty(&self) -> bool {
self.length == 0
}
/// Returns the disjoint union with the other range, if consecutive.
///
/// Appending empty ranges is not possible.
/// Appending to the empty range returns the other range.
///
/// Returns true if successful.
pub fn append(&mut self, other: &ModRange) -> bool {
if self.is_empty() {
self.start = other.start;
self.length = other.length;
return true;
}
if other.is_empty() {
return false;
}
if self.start >= other.start {
return false;
}
if self.length != other.start - self.start {
return false;
}
if let Some(new_length) = self.length.checked_add(other.length) {
self.length = new_length;
true
} else {
false
}
}
/// Helper function to check whether a range starts within another.
fn starts_inside(&self, range: &ModRange) -> bool {
!range.is_empty() && self.start >= range.start && self.start - range.start < range.length
}
/// Returns whether the given range has intersects.
///
/// Mathematically, we calculate whether: `self ∩ range ≠ ∅`.
pub fn intersects_range(&self, range: &ModRange) -> bool {
self.starts_inside(range) || range.starts_inside(self)
}
/// Returns whether the given range is fully contained.
///
/// Mathematically, we calculate whether: `self ∩ range = range`.
pub fn contains_range(&self, range: &ModRange) -> bool {
range.is_empty()
|| (self.start <= range.start
&& range.length <= self.length
&& range.start - self.start <= self.length - range.length)
}
/// Returns an iterator for all contained numbers that are divisible by the modulus.
pub fn aligned_iter(&self, modulus: usize) -> impl Iterator<Item = usize> {
(self.start..=usize::MAX)
.take(self.length)
// Skip the minimum number of elements to align.
.skip((modulus - self.start % modulus) % modulus)
// Only return aligned elements.
.step_by(modulus)
}
}
#[derive(Default)]
pub struct Partition {
ranges: Vec<ModRange>,
}
impl Partition {
/// Total length of all ranges.
pub fn length(&self) -> usize {
self.ranges.iter().map(|r| r.length()).sum()
}
/// Appends the given range.
///
/// Ranges should be appending with ascending start addresses.
pub fn append(&mut self, range: ModRange) -> bool {
if let Some(last_range) = self.ranges.last_mut() {
if range.start() <= last_range.start()
|| range.start() - last_range.start() < last_range.length()
{
return false;
}
if !last_range.append(&range) {
self.ranges.push(range);
}
} else {
self.ranges.push(range);
}
true
}
/// Returns the start address that corresponds to the given offset.
///
/// If the offset bigger than the accumulated length or the requested slice doesn't fit a
/// connected component, return `None`.
pub fn find_address(&self, mut offset: usize, length: usize) -> Option<usize> {
for range in &self.ranges {
if offset < range.length() {
return if range.length() - offset >= length {
Some(range.start() + offset)
} else {
None
};
}
offset -= range.length()
}
None
}
pub fn ranges_from(&self, start_address: usize) -> Vec<ModRange> {
let mut result = Vec::new();
for range in &self.ranges {
match start_address.checked_sub(range.start()) {
None | Some(0) => result.push(range.clone()),
Some(offset) => {
if range.length() > offset {
result.push(ModRange::new(start_address, range.length() - offset));
}
}
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_slice_ok() {
assert_eq!(
find_slice(&[&[1, 2, 3, 4]], 0, 4).ok(),
Some(&[1u8, 2, 3, 4] as &[u8])
);
assert_eq!(
find_slice(&[&[1, 2, 3, 4], &[5, 6]], 1, 2).ok(),
Some(&[2u8, 3] as &[u8])
);
assert_eq!(
find_slice(&[&[1, 2, 3, 4], &[5, 6]], 4, 2).ok(),
Some(&[5u8, 6] as &[u8])
);
assert_eq!(
find_slice(&[&[1, 2, 3, 4], &[5, 6]], 4, 0).ok(),
Some(&[] as &[u8])
);
assert!(find_slice(&[], 0, 1).is_err());
assert!(find_slice(&[&[1, 2, 3, 4], &[5, 6]], 6, 0).is_err());
assert!(find_slice(&[&[1, 2, 3, 4], &[5, 6]], 3, 2).is_err());
}
#[test]
fn alignment() {
for exponent in 0..8 {
let block_size = 1 << exponent;
for i in 0..10 {
assert!(is_aligned(block_size, block_size * i));
}
for i in 1..block_size {
assert!(!is_aligned(block_size, block_size + i));
}
}
}
#[test]
fn mod_range_parameters() {
let range = ModRange::new(200, 100);
assert_eq!(range.start(), 200);
assert_eq!(range.length(), 100);
assert_eq!(ModRange::new_empty().length(), 0);
}
#[test]
fn mod_range_is_empty() {
assert!(!ModRange::new(200, 100).is_empty());
assert!(ModRange::new(200, 0).is_empty());
assert!(ModRange::new_empty().is_empty());
assert!(!ModRange::new(usize::MAX, 2).is_empty());
}
#[test]
fn mod_range_append() {
let mut range = ModRange::new(200, 100);
assert!(range.append(&ModRange::new(300, 400)));
assert!(range.start() == 200);
assert!(range.length() == 500);
assert!(!range.append(&ModRange::new(499, 400)));
assert!(!range.append(&ModRange::new(501, 400)));
assert!(!range.append(&ModRange::new(300, 400)));
let mut range = ModRange::new_empty();
assert!(range.append(&ModRange::new(200, 100)));
assert!(range.start() == 200);
assert!(range.length() == 100);
}
#[test]
fn mod_range_contains_range() {
let range = ModRange::new(200, 100);
assert!(!range.contains_range(&ModRange::new(199, 100)));
assert!(!range.contains_range(&ModRange::new(201, 100)));
assert!(!range.contains_range(&ModRange::new(199, 99)));
assert!(!range.contains_range(&ModRange::new(202, 99)));
assert!(!range.contains_range(&ModRange::new(200, 101)));
assert!(range.contains_range(&ModRange::new(200, 100)));
assert!(range.contains_range(&ModRange::new(200, 99)));
assert!(range.contains_range(&ModRange::new(201, 99)));
assert!(ModRange::new_empty().contains_range(&ModRange::new_empty()));
assert!(ModRange::new(usize::MAX, 1).contains_range(&ModRange::new(usize::MAX, 1)));
assert!(ModRange::new(usize::MAX, 2).contains_range(&ModRange::new(usize::MAX, 2)));
}
#[test]
fn mod_range_intersects_range() {
let range = ModRange::new(200, 100);
assert!(range.intersects_range(&ModRange::new(200, 1)));
assert!(range.intersects_range(&ModRange::new(299, 1)));
assert!(!range.intersects_range(&ModRange::new(199, 1)));
assert!(!range.intersects_range(&ModRange::new(300, 1)));
assert!(!ModRange::new_empty().intersects_range(&ModRange::new_empty()));
assert!(!ModRange::new_empty().intersects_range(&ModRange::new(200, 100)));
assert!(!ModRange::new(200, 100).intersects_range(&ModRange::new_empty()));
assert!(ModRange::new(usize::MAX, 1).intersects_range(&ModRange::new(usize::MAX, 1)));
assert!(ModRange::new(usize::MAX, 2).intersects_range(&ModRange::new(usize::MAX, 2)));
}
#[test]
fn mod_range_aligned_iter() {
let mut iter = ModRange::new(200, 100).aligned_iter(100);
assert_eq!(iter.next(), Some(200));
assert_eq!(iter.next(), None);
let mut iter = ModRange::new(200, 101).aligned_iter(100);
assert_eq!(iter.next(), Some(200));
assert_eq!(iter.next(), Some(300));
assert_eq!(iter.next(), None);
let mut iter = ModRange::new(199, 100).aligned_iter(100);
assert_eq!(iter.next(), Some(200));
assert_eq!(iter.next(), None);
let mut iter = ModRange::new(201, 99).aligned_iter(100);
assert_eq!(iter.next(), None);
let mut iter = ModRange::new(usize::MAX - 16, 20).aligned_iter(16);
assert_eq!(iter.next(), Some(0xffff_ffff_ffff_fff0));
assert_eq!(iter.next(), None);
}
#[test]
fn partition_append() {
let mut partition = Partition::default();
partition.append(ModRange::new(0x4000, 0x1000));
partition.append(ModRange::new(0x20000, 0x20000));
partition.append(ModRange::new(0x40000, 0x20000));
assert_eq!(partition.find_address(0, 1), Some(0x4000));
assert_eq!(partition.length(), 0x41000);
}
#[test]
fn partition_find_address() {
let mut partition = Partition::default();
partition.append(ModRange::new(0x4000, 0x1000));
partition.append(ModRange::new(0x20000, 0x20000));
partition.append(ModRange::new(0x40000, 0x20000));
assert_eq!(partition.find_address(0, 0x1000), Some(0x4000));
assert_eq!(partition.find_address(0x1000, 0x1000), Some(0x20000));
assert_eq!(partition.find_address(0x20000, 0x1000), Some(0x3F000));
assert_eq!(partition.find_address(0x21000, 0x1000), Some(0x40000));
assert_eq!(partition.find_address(0x40000, 0x1000), Some(0x5F000));
assert_eq!(partition.find_address(0x41000, 0x1000), None);
assert_eq!(partition.find_address(0x40000, 0x2000), None);
}
#[test]
fn partition_ranges_from() {
let mut partition = Partition::default();
partition.append(ModRange::new(0x4000, 0x1000));
partition.append(ModRange::new(0x20000, 0x20000));
partition.append(ModRange::new(0x40000, 0x20000));
let all_ranges = partition.ranges_from(0);
let from_start_ranges = partition.ranges_from(0x4000);
assert_eq!(&all_ranges, &from_start_ranges);
assert_eq!(all_ranges.len(), 2);
assert_eq!(all_ranges[0], ModRange::new(0x4000, 0x1000));
assert_eq!(all_ranges[1], ModRange::new(0x20000, 0x40000));
let second_range = partition.ranges_from(0x20000);
let same_second_range = partition.ranges_from(0x1F000);
assert_eq!(&second_range, &same_second_range);
assert_eq!(&second_range, &all_ranges[1..]);
let partial_range = partition.ranges_from(0x30000);
assert_eq!(partial_range[0], ModRange::new(0x30000, 0x30000));
}
}

219
src/env/tock/upgrade_helper.rs vendored Normal file
View File

@@ -0,0 +1,219 @@
// 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.
// For compiling with std outside of tests.
#![cfg_attr(feature = "std", allow(dead_code))]
#[cfg(feature = "std")]
use crate::env::tock::buffer_upgrade_storage::BufferUpgradeStorage;
#[cfg(not(feature = "std"))]
use crate::env::tock::storage::TockUpgradeStorage;
use arrayref::array_ref;
use byteorder::{ByteOrder, LittleEndian};
use opensk::api::crypto::ecdsa::{PublicKey as _, Signature as _};
use opensk::env::{EcdsaPk, EcdsaSignature, Env};
use persistent_store::{StorageError, StorageResult};
pub const METADATA_SIGN_OFFSET: usize = 0x800;
/// 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!
pub fn check_metadata<E: Env>(
#[cfg(not(feature = "std"))] upgrade_locations: &TockUpgradeStorage,
#[cfg(feature = "std")] upgrade_locations: &BufferUpgradeStorage,
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::<E>(
array_ref!(metadata, 32, 64),
public_key_bytes,
parse_metadata_hash(metadata),
)?;
Ok(())
}
/// Parses the metadata, returns the hash.
pub fn parse_metadata_hash(data: &[u8]) -> &[u8; 32] {
array_ref!(data, 0, 32)
}
/// Parses the metadata, returns the firmware version.
pub 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<E: Env>(
signature_bytes: &[u8; 64],
public_key_bytes: &[u8],
signed_hash: &[u8; 32],
) -> StorageResult<()> {
let signature =
EcdsaSignature::<E>::from_slice(signature_bytes).ok_or(StorageError::CustomError)?;
if public_key_bytes.len() != 65 || public_key_bytes[0] != 0x04 {
return Err(StorageError::CustomError);
}
let x = array_ref!(public_key_bytes, 1, 32);
let y = array_ref!(public_key_bytes, 33, 32);
let public_key = EcdsaPk::<E>::from_coordinates(x, y).ok_or(StorageError::CustomError)?;
if !public_key.verify_prehash(signed_hash, &signature) {
return Err(StorageError::CustomError);
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use arrayref::mut_array_refs;
use opensk::api::crypto::ecdsa::SecretKey as _;
use opensk::api::crypto::sha256::Sha256;
use opensk::api::crypto::{EC_FIELD_SIZE, EC_SIGNATURE_SIZE};
use opensk::env::test::TestEnv;
use opensk::env::{EcdsaSk, Sha};
fn to_uncompressed(public_key: &EcdsaPk<TestEnv>) -> [u8; 1 + 2 * EC_FIELD_SIZE] {
// Formatting according to:
// https://tools.ietf.org/id/draft-jivsov-ecc-compact-05.html#overview
const B0_BYTE_MARKER: u8 = 0x04;
let mut representation = [0; 1 + 2 * EC_FIELD_SIZE];
#[allow(clippy::ptr_offset_with_cast)]
let (marker, x, y) = mut_array_refs![&mut representation, 1, EC_FIELD_SIZE, EC_FIELD_SIZE];
marker[0] = B0_BYTE_MARKER;
public_key.to_coordinates(x, y);
representation
}
#[test]
fn test_check_metadata() {
let mut env = TestEnv::default();
let private_key = EcdsaSk::<TestEnv>::random(env.rng());
let upgrade_locations = BufferUpgradeStorage::new().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 = Sha::<TestEnv>::digest(&signed_over_data);
metadata[..32].copy_from_slice(&signed_hash);
let signature = private_key.sign(&signed_over_data);
let mut signature_bytes = [0; EC_SIGNATURE_SIZE];
signature.to_slice(&mut signature_bytes);
metadata[32..96].copy_from_slice(&signature_bytes);
let public_key = private_key.public_key();
let public_key_bytes = to_uncompressed(&public_key);
assert_eq!(
check_metadata::<TestEnv>(&upgrade_locations, &public_key_bytes, &metadata),
Ok(())
);
// Manipulating the partition address fails.
metadata[METADATA_SIGN_OFFSET + 8] = 0x88;
assert_eq!(
check_metadata::<TestEnv>(&upgrade_locations, &public_key_bytes, &metadata),
Err(StorageError::CustomError)
);
metadata[METADATA_SIGN_OFFSET + 8] = 0x00;
// Wrong metadata length fails.
assert_eq!(
check_metadata::<TestEnv>(
&upgrade_locations,
&public_key_bytes,
&metadata[..METADATA_LEN - 1]
),
Err(StorageError::CustomError)
);
// Manipulating the hash fails.
metadata[0] ^= 0x01;
assert_eq!(
check_metadata::<TestEnv>(&upgrade_locations, &public_key_bytes, &metadata),
Err(StorageError::CustomError)
);
metadata[0] ^= 0x01;
// Manipulating the signature fails.
metadata[32] ^= 0x01;
assert_eq!(
check_metadata::<TestEnv>(&upgrade_locations, &public_key_bytes, &metadata),
Err(StorageError::CustomError)
);
}
#[test]
fn test_verify_signature() {
let mut env = TestEnv::default();
let private_key = EcdsaSk::<TestEnv>::random(env.rng());
let message = [0x44; 64];
let signed_hash = Sha::<TestEnv>::digest(&message);
let signature = private_key.sign(&message);
let mut signature_bytes = [0; EC_SIGNATURE_SIZE];
signature.to_slice(&mut signature_bytes);
let public_key = private_key.public_key();
let mut public_key_bytes = to_uncompressed(&public_key);
assert_eq!(
verify_signature::<TestEnv>(&signature_bytes, &public_key_bytes, &signed_hash),
Ok(())
);
assert_eq!(
verify_signature::<TestEnv>(&signature_bytes, &public_key_bytes, &[0x55; 32]),
Err(StorageError::CustomError)
);
public_key_bytes[0] ^= 0x01;
assert_eq!(
verify_signature::<TestEnv>(&signature_bytes, &public_key_bytes, &signed_hash),
Err(StorageError::CustomError)
);
public_key_bytes[0] ^= 0x01;
signature_bytes[0] ^= 0x01;
assert_eq!(
verify_signature::<TestEnv>(&signature_bytes, &public_key_bytes, &signed_hash),
Err(StorageError::CustomError)
);
}
}