diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d65f8245..b5c622ce4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,12 @@ jobs: - name: Install diod run: | sudo apt install -y diod + - name: Install libssl-dev + # Used by litebox_shim_optee's differential test, which compiles the + # original OP-TEE C key derivation against libcrypto and diffs it + # against the Rust port. + run: | + sudo apt install -y libssl-dev - uses: Swatinem/rust-cache@v2 - name: Cache custom out directories uses: actions/cache@v5 diff --git a/Cargo.lock b/Cargo.lock index da29fbf1a..c2793e6f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1809,6 +1809,7 @@ dependencies = [ "p384", "sha2", "spin 0.10.0", + "tempfile", "thiserror", "zerocopy", "zeroize", diff --git a/litebox_shim_optee/Cargo.toml b/litebox_shim_optee/Cargo.toml index 8e6a88e02..cba71d168 100644 --- a/litebox_shim_optee/Cargo.toml +++ b/litebox_shim_optee/Cargo.toml @@ -34,3 +34,4 @@ workspace = true [dev-dependencies] litebox_platform_multiplex = { path = "../litebox_platform_multiplex/", version = "0.1.0", default-features = false, features = ["platform_linux_userland_with_optee_syscall"] } +tempfile = "3" diff --git a/litebox_shim_optee/src/keystack.rs b/litebox_shim_optee/src/keystack.rs new file mode 100644 index 000000000..d96f0b4a2 --- /dev/null +++ b/litebox_shim_optee/src/keystack.rs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Implementation of the SVN key stack pseudo TA. +//! +//! This PTA derives a stack of TA-unique keys, one per Secure Version Number (SVN), +//! so that a TA running at SVN `n` can unseal data sealed by any of its older +//! versions while remaining unable to derive the keys of any newer version. + +use crate::syscalls::Cleanup; +use crate::syscalls::pta::{ + HmacSha256, PTA_DEFAULT_FLAGS, TA_DERIVED_EXTRA_DATA_MAX_SIZE, TA_DERIVED_KEY_MAX_SIZE, + TA_DERIVED_KEY_MIN_SIZE, huk_subkey_derive, open_default_pta_session, +}; +use crate::{Task, UserConstPtr, UserMutPtr}; +use alloc::{vec, vec::Vec}; +use hmac::Mac; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox::utils::TruncateExt; +use litebox_common_optee::{HukSubkeyUsage, TaFlags, TeeParamType, TeeResult, TeeUuid, UteeParams}; +use num_enum::TryFromPrimitive; +use zeroize::Zeroizing; + +/// Maximum number of keys in SVN key stack. +const SVN_KEY_STACK_MAX_SIZE: u32 = 4096; + +pub(crate) struct KeyStackPta; + +/// Command IDs accepted by [`KeyStackPta`]. +#[derive(Clone, Copy, TryFromPrimitive)] +#[repr(u32)] +pub(crate) enum KeyStackCommandId { + DeriveTaSvnKeyStack = 0, +} + +impl KeyStackPta { + pub(crate) const FLAGS: TaFlags = PTA_DEFAULT_FLAGS.union(TaFlags::CONCURRENT); + + // TODO: Replace this placeholder with the UUID agreed upon with the + // consuming TA before this PTA is treated as a stable interface. + pub(crate) const UUID: TeeUuid = TeeUuid { + time_low: 0x978a_f7a7, + time_mid: 0x074f, + time_hi_and_version: 0x4f59, + clock_seq_and_node: [0xb3, 0xae, 0x33, 0xa5, 0x93, 0xc1, 0xd4, 0x88], + }; + + pub(crate) fn open_session(params: &UteeParams) -> Result { + open_default_pta_session(params) + } + + pub(crate) fn close_session(_task: &Task, _session_id: u32) { + // The key stack PTA has no per-session state. + } + + pub(crate) fn invoke_command( + task: &Task, + cmd_id: u32, + params: &mut UteeParams, + ) -> Result { + match KeyStackCommandId::try_from(cmd_id).map_err(|_| TeeResult::BadParameters)? { + KeyStackCommandId::DeriveTaSvnKeyStack => { + Self::derive_ta_svn_key_stack(task, params).map(|()| Cleanup::None) + } + } + } + + /// Derives a stack of unique keys for a TA, one for each possible + /// Secure Version Number (SVN) value up to a maximum. + /// + /// The key derivation follows a two-stage process: + /// 1. First stage: KDF(huk, uuid || extra_data) -> base key + /// 2. Second stage: Iterate from max SVN down to 0, chaining keys: + /// - Key\[max\] = HMAC(base_key, max) + /// - Key\[n\] = HMAC(Key\[n+1\], n) + /// + /// Only keys for SVN values <= current TA version are copied to output. + fn derive_ta_svn_key_stack(task: &Task, params: &mut UteeParams) -> Result<(), TeeResult> { + use TeeParamType::{MemrefInput, MemrefOutput, None, ValueInput}; + // Validate parameter types: + // [in] params[0].value.a Size of each key + // [in] params[0].value.b Number of keys to derive + // [in] params[1].memref.buffer Extra data for key derivation + // [in] params[1].memref.size Extra data size + // [out] params[2].memref.buffer Output buffer for key stack + // [out] params[2].memref.size Buffer size + if !params.has_types([ValueInput, MemrefInput, MemrefOutput, None]) { + return Err(TeeResult::BadParameters); + } + + let (key_size_u64, svn_key_stack_size_u64) = params + .get_values(0) + .map_err(|_| TeeResult::BadParameters)? + .ok_or(TeeResult::BadParameters)?; + let key_size: usize = key_size_u64.trunc(); + let svn_key_stack_size = + u32::try_from(svn_key_stack_size_u64).map_err(|_| TeeResult::BadParameters)?; + + let (extra_data_addr, extra_data_size_u64) = params + .get_values(1) + .map_err(|_| TeeResult::BadParameters)? + .ok_or(TeeResult::BadParameters)?; + let extra_data_size: usize = extra_data_size_u64.trunc(); + + let (key_stack_addr, key_stack_buffer_size_u64) = params + .get_values(2) + .map_err(|_| TeeResult::BadParameters)? + .ok_or(TeeResult::BadParameters)?; + + if !(TA_DERIVED_KEY_MIN_SIZE..=TA_DERIVED_KEY_MAX_SIZE).contains(&key_size) + || extra_data_size > TA_DERIVED_EXTRA_DATA_MAX_SIZE + || svn_key_stack_size > SVN_KEY_STACK_MAX_SIZE + || svn_key_stack_size == 0 + || (extra_data_size > 0 && extra_data_addr == 0) + || key_stack_addr == 0 + { + return Err(TeeResult::BadParameters); + } + + // Only keys for SVN <= ta_svn are emitted, and the chain is only + // `svn_key_stack_size` deep, so the buffer need cover just that many. + let ta_svn = task.ta_svn; + let emitted_key_count = ta_svn.saturating_add(1).min(svn_key_stack_size); + let required_stack_buffer_size = key_size + .checked_mul(emitted_key_count as usize) + .ok_or(TeeResult::BadParameters)?; + let required_stack_buffer_size_u64 = + u64::try_from(required_stack_buffer_size).map_err(|_| TeeResult::BadParameters)?; + if key_stack_buffer_size_u64 < required_stack_buffer_size_u64 { + // Report the required size so the caller can probe with a small + // buffer and retry, mirroring the GP short-buffer convention. + params + .set_values(2, key_stack_addr, required_stack_buffer_size_u64) + .map_err(|_| TeeResult::BadParameters)?; + return Err(TeeResult::ShortBuffer); + } + + let extra_data = if extra_data_size == 0 { + Vec::new().into_boxed_slice() + } else { + let extra_data_ptr = UserConstPtr::::from_usize(extra_data_addr.trunc()); + extra_data_ptr + .to_owned_slice(extra_data_size) + .ok_or(TeeResult::BadParameters)? + }; + + // Unlike OP-TEE OS, `UserMutPtr` (and `UserConstPtr`) in LiteBox ensure this + // pointer can never be used to access normal-world memory. That is, we don't + // need extra security check for detecting key leakage here. + let key_stack_ptr = UserMutPtr::::from_usize(key_stack_addr.trunc()); + + // First stage: derive base key = KDF(huk, usage || ta_uuid || extra data) + let uuid_bytes = task.ta_app_id.to_le_bytes(); + let mut stage_key = Zeroizing::new(vec![0u8; key_size]); + huk_subkey_derive( + task, + HukSubkeyUsage::UniqueTa, + &[&uuid_bytes, &extra_data], + &mut stage_key, + )?; + + // Derive keys from max SVN down to 0 + for svn_idx in (0..svn_key_stack_size).rev() { + // Second stage KDF: HMAC(current_key, SVN_index) + // Key_v2047 = KDF(KDF(HUK, UUID), 2047) + // Key_v2046 = KDF(Key_v2047, 2046) + // ... + // Key_v001 = KDF(Key_v002, 001) + // Key_v000 = KDF(Key_v001, 000) + let mut hmac = + HmacSha256::new_from_slice(&stage_key).map_err(|_| TeeResult::BadParameters)?; + hmac.update(&svn_idx.to_le_bytes()); + + let hmac_bytes = Zeroizing::new(hmac.finalize().into_bytes()); + let derived_key = &hmac_bytes[..key_size]; + + // Only copy keys for SVN values <= current TA version to userspace + if svn_idx <= ta_svn { + let offset = svn_idx as usize * key_size; + key_stack_ptr + .copy_from_slice(offset, derived_key) + .ok_or(TeeResult::AccessDenied)?; + } + stage_key.copy_from_slice(derived_key); + } + + // Report how many bytes were actually written. + params + .set_values(2, key_stack_addr, required_stack_buffer_size_u64) + .map_err(|_| TeeResult::BadParameters) + } +} diff --git a/litebox_shim_optee/src/lib.rs b/litebox_shim_optee/src/lib.rs index ce89efbd8..641fc19ed 100644 --- a/litebox_shim_optee/src/lib.rs +++ b/litebox_shim_optee/src/lib.rs @@ -39,6 +39,8 @@ pub mod msg_handler; #[cfg(feature = "platform_lvbs")] pub mod idk; +pub(crate) mod keystack; + // Re-export session management types for convenience pub use session::{OpenSessionTarget, SessionManager, SessionToken, TaInstance}; @@ -265,6 +267,8 @@ impl OpteeShim { global: self.0.clone(), thread: ThreadState::new(), ta_app_id: ta_uuid, + // TODO: Populate this from trusted TA version metadata when available. + ta_svn: 0, tee_cryp_state_map: TeeCrypStateMap::new(), tee_obj_map: TeeObjMap::new(), ta_handle_map: TaHandleMap::new(), @@ -1376,6 +1380,8 @@ struct Task { thread: ThreadState, /// TA UUID ta_app_id: TeeUuid, + /// TA Secure Version Number (SVN) + ta_svn: u32, /// TEE cryptography state map tee_cryp_state_map: TeeCrypStateMap, /// TEE object map @@ -1546,10 +1552,20 @@ mod test_utils { impl GlobalState { /// Make a new task with default values for testing. pub(crate) fn new_test_task(self: Arc) -> Task { + self.new_test_task_with_uuid_and_svn(TeeUuid::default(), 0) + } + + /// Make a new task with the provided TA UUID and SVN for testing. + pub(crate) fn new_test_task_with_uuid_and_svn( + self: Arc, + ta_app_id: TeeUuid, + ta_svn: u32, + ) -> Task { Task { global: self.clone(), thread: ThreadState::new(), - ta_app_id: TeeUuid::default(), + ta_app_id, + ta_svn, tee_cryp_state_map: TeeCrypStateMap::new(), tee_obj_map: TeeObjMap::new(), ta_handle_map: TaHandleMap::new(), diff --git a/litebox_shim_optee/src/syscalls/pta.rs b/litebox_shim_optee/src/syscalls/pta.rs index ab0c426ba..5b3053b62 100644 --- a/litebox_shim_optee/src/syscalls/pta.rs +++ b/litebox_shim_optee/src/syscalls/pta.rs @@ -4,6 +4,7 @@ //! Implementation of pseudo TAs (PTAs) which export system services as //! the functions of built-in TAs. +use crate::keystack::KeyStackPta; use crate::syscalls::Cleanup; use crate::{Task, UserConstPtr, UserMutPtr}; use alloc::vec; @@ -30,12 +31,14 @@ struct SystemPta; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub(crate) enum PseudoTa { System, + KeyStack, } impl PseudoTa { pub(crate) fn from_uuid(uuid: &TeeUuid) -> Option { match *uuid { SystemPta::UUID => Some(Self::System), + KeyStackPta::UUID => Some(Self::KeyStack), _ => None, } } @@ -44,6 +47,7 @@ impl PseudoTa { fn open_session(self, params: &UteeParams) -> Result { match self { Self::System => SystemPta::open_session(params), + Self::KeyStack => KeyStackPta::open_session(params), } } @@ -56,28 +60,46 @@ impl PseudoTa { let _busy = task.try_set_busy(self)?; match self { Self::System => SystemPta::invoke_command(task, cmd_id, params), + Self::KeyStack => KeyStackPta::invoke_command(task, cmd_id, params), } } fn close_session(self, task: &Task, session_id: u32) { match self { Self::System => SystemPta::close_session(task, session_id), + Self::KeyStack => KeyStackPta::close_session(task, session_id), } } fn flags(self) -> TaFlags { match self { Self::System => SystemPta::FLAGS, + Self::KeyStack => KeyStackPta::FLAGS, } } } -const PTA_DEFAULT_FLAGS: TaFlags = TaFlags::SINGLE_INSTANCE +pub(crate) const PTA_DEFAULT_FLAGS: TaFlags = TaFlags::SINGLE_INSTANCE .union(TaFlags::MULTI_SESSION) .union(TaFlags::INSTANCE_KEEP_ALIVE); const MAX_PTA_SESSIONS_PER_TASK: usize = 100; +/// Open a session to a PTA that carries no per-session state and takes no +/// parameters at session-open time. +pub(crate) fn open_default_pta_session(params: &UteeParams) -> Result { + if !params.has_types([ + TeeParamType::None, + TeeParamType::None, + TeeParamType::None, + TeeParamType::None, + ]) { + return Err(TeeResult::BadParameters); + } + + crate::SessionIdPool::allocate().ok_or(TeeResult::Busy) +} + struct PtaBusyGuard<'a> { task: &'a Task, pta: PseudoTa, @@ -105,11 +127,11 @@ const PTA_SYSTEM_GET_TPM_EVENT_LOG: u32 = 12; const PTA_SYSTEM_SUPP_PLUGIN_INVOKE: u32 = 13; /// Minimum size of a derived key in bytes. -const TA_DERIVED_KEY_MIN_SIZE: usize = 16; +pub(crate) const TA_DERIVED_KEY_MIN_SIZE: usize = 16; /// Maximum size of a derived key in bytes. -const TA_DERIVED_KEY_MAX_SIZE: usize = 32; +pub(crate) const TA_DERIVED_KEY_MAX_SIZE: usize = 32; /// Maximum size of extra data for key derivation in bytes. -const TA_DERIVED_EXTRA_DATA_MAX_SIZE: usize = 1024; +pub(crate) const TA_DERIVED_EXTRA_DATA_MAX_SIZE: usize = 1024; /// `PTA_SYSTEM_*` command ID from `optee_os/lib/libutee/include/pta_system.h` #[derive(Clone, Copy, TryFromPrimitive)] @@ -131,7 +153,7 @@ enum PtaSystemCommandId { SuppPluginInvoke = PTA_SYSTEM_SUPP_PLUGIN_INVOKE, } -type HmacSha256 = Hmac; +pub(crate) type HmacSha256 = Hmac; impl Task { /// Try to mark a non-concurrent PTA as busy, returning a guard that clears @@ -221,16 +243,7 @@ impl SystemPta { }; fn open_session(params: &UteeParams) -> Result { - if !params.has_types([ - TeeParamType::None, - TeeParamType::None, - TeeParamType::None, - TeeParamType::None, - ]) { - return Err(TeeResult::BadParameters); - } - - crate::SessionIdPool::allocate().ok_or(TeeResult::Busy) + open_default_pta_session(params) } fn close_session(_task: &Task, _session_id: u32) { @@ -308,7 +321,7 @@ impl SystemPta { // subkey = KDF(huk, usage || ta_uuid || extra_data) let ta_uuid_bytes = task.ta_app_id.to_le_bytes(); let mut subkey_buf = Zeroizing::new(vec![0u8; subkey_size]); - Self::huk_subkey_derive( + huk_subkey_derive( task, HukSubkeyUsage::UniqueTa, &[&ta_uuid_bytes, &extra_data], @@ -321,44 +334,6 @@ impl SystemPta { }) } - /// Derive a subkey using HUK and constant data. - /// - /// This follows the OP-TEE `huk_subkey_derive` interface from `core/kernel/huk_subkey.c`. - fn huk_subkey_derive( - task: &Task, - usage: HukSubkeyUsage, - const_data: &[&[u8]], - subkey: &mut [u8], - ) -> Result<(), TeeResult> { - let subkey_len = subkey.len(); - if subkey_len > HUK_SUBKEY_MAX_LEN { - return Err(TeeResult::BadParameters); - } - - let kdf_context_len = - core::mem::size_of::() + const_data.iter().map(|chunk| chunk.len()).sum::(); - let mut kdf_context = Zeroizing::new(Vec::with_capacity(kdf_context_len)); - kdf_context.extend_from_slice(&(usage as u32).to_le_bytes()); - for chunk in const_data { - kdf_context.extend_from_slice(chunk); - } - let kdf_params = KDFParams { - context: kdf_context.as_slice(), - output: subkey, - }; - - task.global - .platform - .derive_key(Some(huk_subkey_derive_inner), kdf_params) - .map_err(|err| match err { - DerivedKeyError::ShimKDFRequired - | DerivedKeyError::UnsupportedRebootPersistentKey => TeeResult::NotSupported, - DerivedKeyError::ShimKDFError(err) => err, - })?; - - Ok(()) - } - fn map_zi(task: &Task, params: &mut UteeParams) -> Result { use TeeParamType::{None, ValueInout, ValueInput}; @@ -435,6 +410,45 @@ impl SystemPta { } } +/// Derive a subkey using HUK and constant data. +/// +/// This follows the OP-TEE `huk_subkey_derive` interface from `core/kernel/huk_subkey.c`. +pub(crate) fn huk_subkey_derive( + task: &Task, + usage: HukSubkeyUsage, + const_data: &[&[u8]], + subkey: &mut [u8], +) -> Result<(), TeeResult> { + let subkey_len = subkey.len(); + if subkey_len > HUK_SUBKEY_MAX_LEN { + return Err(TeeResult::BadParameters); + } + + let kdf_context_len = + core::mem::size_of::() + const_data.iter().map(|chunk| chunk.len()).sum::(); + let mut kdf_context = Zeroizing::new(Vec::with_capacity(kdf_context_len)); + kdf_context.extend_from_slice(&(usage as u32).to_le_bytes()); + for chunk in const_data { + kdf_context.extend_from_slice(chunk); + } + let kdf_params = KDFParams { + context: kdf_context.as_slice(), + output: subkey, + }; + + task.global + .platform + .derive_key(Some(huk_subkey_derive_inner), kdf_params) + .map_err(|err| match err { + DerivedKeyError::ShimKDFRequired | DerivedKeyError::UnsupportedRebootPersistentKey => { + TeeResult::NotSupported + } + DerivedKeyError::ShimKDFError(err) => err, + })?; + + Ok(()) +} + /// A KDF callback that derives a subkey from `huk` and `params.context` to be passed to /// the underlying platform implementation of `derive_key`. fn huk_subkey_derive_inner(huk: &[u8], params: KDFParams<'_>) -> Result<(), TeeResult> { diff --git a/litebox_shim_optee/src/syscalls/tests.rs b/litebox_shim_optee/src/syscalls/tests.rs index 441228918..2072fd526 100644 --- a/litebox_shim_optee/src/syscalls/tests.rs +++ b/litebox_shim_optee/src/syscalls/tests.rs @@ -1,17 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use crate::keystack::KeyStackCommandId; +use crate::syscalls::pta::{ + PseudoTa, TA_DERIVED_EXTRA_DATA_MAX_SIZE, TA_DERIVED_KEY_MAX_SIZE, TA_DERIVED_KEY_MIN_SIZE, +}; +extern crate std; + +use alloc::{vec, vec::Vec}; +use litebox_common_optee::TeeUuid; +use litebox_common_optee::{TeeParamType, TeeResult, UteeParams}; use litebox_platform_multiplex::{Platform, set_platform}; // Ensure we only init the platform once static INIT_FUNC: spin::Once = spin::Once::new(); -#[must_use] -#[cfg_attr( - not(target_os = "linux"), - expect(unused_variables, reason = "ignored parameter on non-linux platforms") -)] -pub(crate) fn init_platform() -> crate::Task { +fn ensure_platform() { INIT_FUNC.call_once(|| { #[cfg(target_os = "linux")] let platform = Platform::new(None); @@ -19,8 +23,17 @@ pub(crate) fn init_platform() -> crate::Task { #[cfg(not(target_os = "linux"))] let platform = Platform::new(); + // Required for `derive_key` to succeed; the key-stack tests depend on it. + #[cfg(target_os = "linux")] + platform.initialize_boot_specific_kdf_support(); + set_platform(platform); }); +} + +#[must_use] +pub(crate) fn init_platform() -> crate::Task { + ensure_platform(); let shim_builder = crate::OpteeShimBuilder::new(); let _litebox = shim_builder.litebox(); @@ -66,3 +79,310 @@ fn test_sys_get_time_system_is_monotonic() { let second_ms = u64::from(second.seconds) * 1000 + u64::from(second.millis); assert!(second_ms >= first_ms, "system time went backwards"); } + +// --------------------------------------------------------------------------- +// Key stack PTA: differential test against the C original +// --------------------------------------------------------------------------- + +const KEY_STACK_CMD: u32 = KeyStackCommandId::DeriveTaSvnKeyStack as u32; + +/// A single differential case: the inputs to one key stack derivation. +struct Case { + key_size: u32, + stack_size: u32, + ta_svn: u32, + uuid: [u8; 16], + extra_data: Vec, +} + +/// Compile `tests/keystack_ref.c`, the re-hosted C original, into `dir` and +/// return the path to the resulting binary. +fn compile_c_reference(dir: &std::path::Path) -> std::path::PathBuf { + let src = alloc::format!("{}/tests/keystack_ref.c", env!("CARGO_MANIFEST_DIR")); + let out = dir.join("keystack_ref"); + let output = std::process::Command::new("gcc") + .args(["-O2", "-Wall", "-Wextra", "-Werror", "-o"]) + .arg(&out) + .arg(&src) + .arg("-lcrypto") + .output() + .unwrap_or_else(|e| panic!("failed to run gcc (is a C toolchain installed?): {e}")); + assert!( + output.status.success(), + "failed to compile {src} (needs gcc and libssl-dev):\n{}", + std::string::String::from_utf8_lossy(&output.stderr) + ); + out +} + +/// Run the C reference over `cases`, returning one ` ` +/// line per case. +fn run_c_reference(cases: &[Case]) -> Vec { + use alloc::string::{String, ToString}; + use core::fmt::Write as _; + + // Scratch space for the compiled reference and its input. + let dir = tempfile::tempdir().expect("create temp dir"); + let bin = compile_c_reference(dir.path()); + + let mut input = String::new(); + for c in cases { + let mut uuid = String::new(); + for b in &c.uuid { + write!(uuid, "{b:02x}").unwrap(); + } + let mut extra = String::new(); + for b in &c.extra_data { + write!(extra, "{b:02x}").unwrap(); + } + writeln!( + input, + "{} {} {} {} {}", + c.key_size, + c.stack_size, + c.ta_svn, + uuid, + if extra.is_empty() { "-" } else { &extra } + ) + .unwrap(); + } + + let cases_path = dir.path().join("cases"); + std::fs::write(&cases_path, input).expect("write cases file"); + + let output = std::process::Command::new(&bin) + .arg(&cases_path) + .output() + .expect("run C reference"); + assert!( + output.status.success(), + "C reference failed:\n{}", + std::string::String::from_utf8_lossy(&output.stderr) + ); + + std::string::String::from_utf8(output.stdout) + .expect("C output is utf-8") + .lines() + .map(ToString::to_string) + .collect() +} + +/// Run one case through the actual PTA, formatted like the C reference output. +fn run_rust_implementation(case: &Case) -> alloc::string::String { + use alloc::string::String; + use core::fmt::Write as _; + + let shim_builder = crate::OpteeShimBuilder::new(); + let _litebox = shim_builder.litebox(); + let task = shim_builder.build().0.new_test_task_with_uuid_and_svn( + TeeUuid { + time_low: u32::from_le_bytes(case.uuid[0..4].try_into().unwrap()), + time_mid: u16::from_le_bytes(case.uuid[4..6].try_into().unwrap()), + time_hi_and_version: u16::from_le_bytes(case.uuid[6..8].try_into().unwrap()), + clock_seq_and_node: case.uuid[8..16].try_into().unwrap(), + }, + case.ta_svn, + ); + + // The C sizes its output buffer `key_size * (ta_svn + 1)`; match it so the + // full buffer, including any region the C leaves zeroed, is compared. + let buf_len = case.key_size as usize * (case.ta_svn as usize + 1); + let mut key_buf = vec![0u8; buf_len]; + let mut params = key_stack_params( + u64::from(case.key_size), + u64::from(case.stack_size), + &case.extra_data, + &mut key_buf, + ); + + let mut out = String::new(); + match invoke_key_stack(&task, KEY_STACK_CMD, &mut params) { + Ok(()) => { + out.push_str("00000000 "); + for b in &key_buf { + write!(out, "{b:02x}").unwrap(); + } + } + Err(e) => write!(out, "{:08x} -", u32::from(e)).unwrap(), + } + out +} + +/// Invoke the key stack PTA, discarding the (always-`None`) cleanup token so +/// the result can be compared/unwrapped in assertions. +fn invoke_key_stack( + task: &crate::Task, + cmd_id: u32, + params: &mut UteeParams, +) -> Result<(), TeeResult> { + PseudoTa::KeyStack + .invoke_command(task, cmd_id, params) + .map(|_| ()) +} + +/// Build a well-formed [`KeyStackCommandId::DeriveTaSvnKeyStack`] parameter set. +fn key_stack_params( + key_size: u64, + stack_size: u64, + extra_data: &[u8], + key_buf: &mut [u8], +) -> UteeParams { + let mut params = UteeParams::new(); + params.set_type(0, TeeParamType::ValueInput).unwrap(); + params.set_values(0, key_size, stack_size).unwrap(); + params.set_type(1, TeeParamType::MemrefInput).unwrap(); + params + .set_values(1, extra_data.as_ptr() as u64, extra_data.len() as u64) + .unwrap(); + params.set_type(2, TeeParamType::MemrefOutput).unwrap(); + params + .set_values(2, key_buf.as_mut_ptr() as u64, key_buf.len() as u64) + .unwrap(); + params.set_type(3, TeeParamType::None).unwrap(); + params +} + +/// The cases both implementations are run over: hand-picked boundaries first, +/// then a deterministic pseudorandom sweep to cover combinations the boundary +/// list would miss. +fn differential_cases() -> Vec { + let min_key_size = u32::try_from(TA_DERIVED_KEY_MIN_SIZE).unwrap(); + let max_key_size = u32::try_from(TA_DERIVED_KEY_MAX_SIZE).unwrap(); + + let uuid = [ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, + 0x10, + ]; + // (key_size, stack_size, ta_svn, extra_len) + let boundaries: &[(u32, u32, u32, usize)] = &[ + // Every interesting key size, including non-multiples of the digest + // length, which catch bad HMAC truncation. + (16, 8, 0, 16), + (17, 8, 0, 16), + (24, 8, 0, 16), + (31, 8, 0, 16), + (32, 8, 0, 16), + // Extra data boundaries. + (32, 8, 0, 0), + (32, 8, 0, 1), + (32, 8, 0, TA_DERIVED_EXTRA_DATA_MAX_SIZE), + // ta_svn > 0 exercises the `i * key_size` output offsets. + (32, 8, 1, 8), + (32, 8, 3, 8), + (32, 8, 7, 8), + (16, 8, 7, 8), + (32, 16, 5, 8), + // ta_svn >= chain depth: only a suffix of the buffer is written. + (32, 2, 3, 8), + (32, 1, 5, 8), + (32, 4, 10, 8), + // Chain depth boundaries, including the 4096 maximum. + (32, 1, 0, 8), + (32, 4096, 2, 8), + (32, 4095, 0, 8), + // Inputs both implementations must reject, pinning the shared + // parameter validation as well as the derivation. The C and the port + // report the same code for each of these (TEE_ERROR_BAD_PARAMETERS), + // so they compare like any other case. + (min_key_size - 1, 8, 0, 8), // key too small + (max_key_size + 1, 8, 0, 8), // key too large + (32, 8, 0, TA_DERIVED_EXTRA_DATA_MAX_SIZE + 1), // extra data too long + (32, 0, 0, 8), // empty chain + (32, 4097, 0, 8), // chain longer than the 4096 maximum + ]; + + let mut cases: Vec = boundaries + .iter() + .map(|&(key_size, stack_size, ta_svn, extra_len)| Case { + key_size, + stack_size, + ta_svn, + uuid, + extra_data: (0..extra_len) + .map(|i| u8::try_from(i % 256).unwrap()) + .collect(), + }) + .collect(); + + // Deterministic xorshift so failures are reproducible. + let mut state: u64 = 0x2545_f491_4f6c_dd1d; + let mut next = |n: u64| { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + state % n + }; + for _ in 0..200 { + let key_size = 16 + u32::try_from(next(17)).unwrap(); // 16..=32 + let stack_size = 1 + u32::try_from(next(64)).unwrap(); + let ta_svn = u32::try_from(next(16)).unwrap(); + let extra_len = usize::try_from(next(64)).unwrap(); + let mut uuid = [0u8; 16]; + for b in &mut uuid { + *b = u8::try_from(next(256)).unwrap(); + } + cases.push(Case { + key_size, + stack_size, + ta_svn, + uuid, + extra_data: (0..extra_len) + .map(|_| u8::try_from(next(256)).unwrap()) + .collect(), + }); + } + cases +} + +/// Differential test: the Rust port must agree byte for byte with the C +/// original in `tests/keystack_ref.c`, which is compiled and run as part of +/// this test. +/// +/// Both sides derive from the same HUK (the platform feeds the KDF +/// `/proc/sys/kernel/random/boot_id`, which the C reads directly), so the +/// comparison covers the real derivation rather than a restatement of it. +#[test] +fn key_stack_matches_c_reference() { + ensure_platform(); + + let cases = differential_cases(); + let c_output = run_c_reference(&cases); + assert_eq!( + c_output.len(), + cases.len(), + "C reference produced {} lines for {} cases", + c_output.len(), + cases.len() + ); + + let mut derived = 0; + let mut rejected = 0; + for (index, (case, expected)) in cases.iter().zip(c_output.iter()).enumerate() { + let actual = run_rust_implementation(case); + // `differential_cases()` is deterministic, so the index alone is enough + // to identify and reproduce a failing case; the UUID is deliberately + // not reported here to keep derivation inputs out of test output. + assert_eq!( + &actual, + expected, + "case {index} diverged from the C original: key_size={} stack_size={} \ + ta_svn={} extra_data_len={}", + case.key_size, + case.stack_size, + case.ta_svn, + case.extra_data.len() + ); + if expected.starts_with("00000000 ") { + derived += 1; + } else { + rejected += 1; + } + } + + // Guard against a matrix that agrees only because nothing derives a key, + // or one that quietly stops covering the rejection paths. + assert!( + derived > 0 && rejected >= 5, + "derived={derived} rejected={rejected}" + ); +} diff --git a/litebox_shim_optee/tests/keystack_ref.c b/litebox_shim_optee/tests/keystack_ref.c new file mode 100644 index 000000000..241eb358d --- /dev/null +++ b/litebox_shim_optee/tests/keystack_ref.c @@ -0,0 +1,387 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Re-hosted reference implementation of `system_derive_ta_svn_key_stack()` +// from the Microsoft OP-TEE fork, used for differential testing against the +// Rust port in `litebox_shim_optee::keystack`. +// +// The derivation logic in `system_derive_ta_svn_key_stack()` below is copied +// verbatim from the C original. Only the surrounding OP-TEE services are +// re-hosted, none of which affect the derived bytes: +// +// vm_check_access_rights() -> dropped; a memory-safety check, and the Rust +// port relies on UserMutPtr instead. +// system_get_ta_version() -> injected TA SVN. +// huk_subkey_derive() -> HMAC-SHA256(HUK, LE32(usage) || data), per +// optee_os core/kernel/huk_subkey.c. +// crypto_mac_*() -> the HMAC-SHA256 below. +// HUK -> /proc/sys/kernel/random/boot_id, which is the +// key the LiteBox Linux-userland platform feeds +// its KDF. +// +// Usage: keystack_ref +// Each non-comment line is: key_size stack_size ta_svn uuid_hex extra_hex +// ('-' denotes empty extra data). Prints " " +// per case, where the key stack is `key_size * (ta_svn + 1)` bytes. + +#include +#include +#include +#include +#include +#include + +/* ------------------------------------------------------------------ */ +/* HMAC-SHA256 via OpenSSL */ +/* */ +/* OP-TEE's crypto_mac_* API is incremental, so the update calls are */ +/* buffered and hashed in one shot at final. This keeps the call */ +/* structure of the original while avoiding OpenSSL's HMAC_CTX_*, */ +/* which is deprecated since 3.0. */ +/* ------------------------------------------------------------------ */ + +#define MAC_BUF_MAX 8192 + +struct mac_ctx { + uint8_t key[64]; + size_t key_len; + uint8_t buf[MAC_BUF_MAX]; + size_t buf_len; +}; + +static void hmac_sha256(const uint8_t *key, size_t key_len, const uint8_t *data, + size_t data_len, uint8_t *out32) +{ + unsigned int len = 0; + + if (!HMAC(EVP_sha256(), key, (int)key_len, data, data_len, out32, &len) || + len != 32) { + fprintf(stderr, "HMAC-SHA256 failed\n"); + exit(2); + } +} + +/* ------------------------------------------------------------------ */ +/* Re-hosted OP-TEE services */ +/* ------------------------------------------------------------------ */ + +#define TA_DERIVED_KEY_MIN_SIZE 16 +#define TA_DERIVED_KEY_MAX_SIZE 32 +#define TA_DERIVED_EXTRA_DATA_MAX_SIZE 1024 + +#define TEE_SUCCESS 0 +#define TEE_ERROR_GENERIC 0xFFFF0000 +#define TEE_ERROR_BAD_PARAMETERS 0xFFFF0006 +#define TEE_ERROR_OUT_OF_MEMORY 0xFFFF000C +#define TEE_ERROR_SECURITY 0xFFFF000F + +#define HUK_SUBKEY_UNIQUE_TA 3 + +#define EMSG(fmt, ...) fprintf(stderr, fmt "\n", ##__VA_ARGS__) +#define ADD_OVERFLOW(a, b, res) __builtin_add_overflow((a), (b), (res)) + +typedef uint32_t TEE_Result; + +typedef struct { + uint32_t timeLow; + uint16_t timeMid; + uint16_t timeHiAndVersion; + uint8_t clockSeqAndNode[8]; +} TEE_UUID; + +static uint8_t g_huk[16]; +static uint32_t g_ta_version; +static TEE_UUID g_uuid; + +static void free_wipe(void *p) { free(p); } + +/* optee_os core/kernel/huk_subkey.c: HMAC(HUK, usage_as_u32 || const_data) */ +static TEE_Result huk_subkey_derive(uint32_t usage, const uint8_t *data, + size_t data_len, uint8_t *out, size_t out_len) +{ + uint8_t mac[32]; + uint8_t *ctx_buf; + size_t ctx_len = sizeof(uint32_t) + data_len; + + if (out_len > 32) + return TEE_ERROR_BAD_PARAMETERS; + + ctx_buf = calloc(ctx_len, 1); + if (!ctx_buf) + return TEE_ERROR_OUT_OF_MEMORY; + ctx_buf[0] = (uint8_t)(usage & 0xff); + ctx_buf[1] = (uint8_t)((usage >> 8) & 0xff); + ctx_buf[2] = (uint8_t)((usage >> 16) & 0xff); + ctx_buf[3] = (uint8_t)((usage >> 24) & 0xff); + memcpy(ctx_buf + sizeof(uint32_t), data, data_len); + + hmac_sha256(g_huk, sizeof(g_huk), ctx_buf, ctx_len, mac); + free(ctx_buf); + memcpy(out, mac, out_len); + return TEE_SUCCESS; +} + +static TEE_Result system_get_ta_version(uint32_t *v) +{ + *v = g_ta_version; + return TEE_SUCCESS; +} + +static TEE_Result crypto_mac_alloc_ctx(void **ctx, int alg) +{ + (void)alg; + *ctx = calloc(1, sizeof(struct mac_ctx)); + return *ctx ? TEE_SUCCESS : TEE_ERROR_OUT_OF_MEMORY; +} + +static void crypto_mac_free_ctx(void *ctx) { free(ctx); } + +static TEE_Result crypto_mac_init(void *ctx, const uint8_t *key, size_t len) +{ + struct mac_ctx *m = ctx; + + if (len > sizeof(m->key)) + return TEE_ERROR_BAD_PARAMETERS; + memcpy(m->key, key, len); + m->key_len = len; + m->buf_len = 0; + return TEE_SUCCESS; +} + +static TEE_Result crypto_mac_update(void *ctx, const uint8_t *d, size_t len) +{ + struct mac_ctx *m = ctx; + + if (m->buf_len + len > sizeof(m->buf)) + return TEE_ERROR_OUT_OF_MEMORY; + memcpy(m->buf + m->buf_len, d, len); + m->buf_len += len; + return TEE_SUCCESS; +} + +static TEE_Result crypto_mac_final(void *ctx, uint8_t *out, size_t len) +{ + struct mac_ctx *m = ctx; + uint8_t mac[32]; + + if (len > 32) + return TEE_ERROR_SECURITY; /* OP-TEE: short buffer */ + hmac_sha256(m->key, m->key_len, m->buf, m->buf_len, mac); + memcpy(out, mac, len); + return TEE_SUCCESS; +} + +/* ------------------------------------------------------------------ */ +/* The function under test -- body unchanged from the fork */ +/* ------------------------------------------------------------------ */ + +static TEE_Result system_derive_ta_svn_key_stack(uint32_t key_size, + uint32_t svn_key_stack_size, + const uint8_t *extra_data_buffer, + size_t extra_data_size, + uint8_t *key_stack, + size_t svn_key_buffer_size) +{ + TEE_Result res = TEE_ERROR_GENERIC; + uint32_t ta_version; + uint8_t stg1_temp_key[TA_DERIVED_KEY_MAX_SIZE]; + uint8_t stg2_temp_key[TA_DERIVED_KEY_MAX_SIZE]; + void *ctx = NULL; + size_t data_len; + uint8_t *data = NULL; + + if (key_size < TA_DERIVED_KEY_MIN_SIZE || + key_size > TA_DERIVED_KEY_MAX_SIZE || + extra_data_size > TA_DERIVED_EXTRA_DATA_MAX_SIZE || + svn_key_stack_size > 4096 || + svn_key_stack_size == 0 || + !key_stack) { + EMSG("%s bad parameters", __func__); + return TEE_ERROR_BAD_PARAMETERS; + } + + res = system_get_ta_version(&ta_version); + if (res) { + EMSG("%s unable to get TA version", __func__); + return res; + } + if (svn_key_buffer_size < (key_size * (ta_version + 1))) { + EMSG("%s key stack is too small", __func__); + return TEE_ERROR_BAD_PARAMETERS; + } + for (int i = svn_key_stack_size - 1; i >= 0 ; i--) { + if (i == (int)(svn_key_stack_size - 1)) { + data_len = sizeof(TEE_UUID); + /* Take extra data into account. */ + if (ADD_OVERFLOW(data_len, extra_data_size, &data_len)) + return TEE_ERROR_SECURITY; + data = calloc(data_len, 1); + if (!data) + return TEE_ERROR_OUT_OF_MEMORY; + memcpy(data, &g_uuid, sizeof(TEE_UUID)); + /* Append the user provided data */ + memcpy(data + sizeof(TEE_UUID), extra_data_buffer, + extra_data_size); + /* First iteration KDF: KDF(HUK, UUID) */ + res = huk_subkey_derive(HUK_SUBKEY_UNIQUE_TA, + data, + data_len, + stg1_temp_key, + key_size); + free_wipe(data); + if (res) + return res; + } else { + /* Add previous key */ + memcpy(stg1_temp_key, stg2_temp_key, key_size); + } + /* + * Second iteration KDF: + * Key_v2047 = KDF(KDF(HUK, UUID), 2047) + * Key_v2046 = KDF(Key_v2047, 2046) + * ... + * Key_v000 = KDF(Key_v001, 000) + */ + res = crypto_mac_alloc_ctx(&ctx, 0); + if (res) + return res; + res = crypto_mac_init(ctx, stg1_temp_key, key_size); + if (res) + goto err; + /* Add the SVN */ + res = crypto_mac_update(ctx, (uint8_t *)&i, sizeof(i)); + if (res) + goto err; + res = crypto_mac_final(ctx, stg2_temp_key, key_size); + if (res) + goto err; + crypto_mac_free_ctx(ctx); + if (i <= (int)ta_version) { + /* Copy to output stack */ + memcpy(key_stack + (i * key_size), + stg2_temp_key, + key_size); + } + } + return res; +err: + crypto_mac_free_ctx(ctx); + return res; +} + +/* ------------------------------------------------------------------ */ +/* Harness */ +/* ------------------------------------------------------------------ */ + +static void load_huk(void) +{ + FILE *f = fopen("/proc/sys/kernel/random/boot_id", "r"); + char buf[64]; + int n = 0; + char *p; + + if (!f || !fgets(buf, sizeof(buf), f)) { + fprintf(stderr, "cannot read boot_id\n"); + exit(1); + } + fclose(f); + for (p = buf; *p && n < 16; p++) { + unsigned v; + + if (*p == '-' || *p == '\n') + continue; + if (sscanf(p, "%2x", &v) != 1) { + fprintf(stderr, "malformed boot_id\n"); + exit(1); + } + g_huk[n++] = (uint8_t)v; + p++; + } + if (n != 16) { + fprintf(stderr, "short boot_id (%d bytes)\n", n); + exit(1); + } +} + +static size_t hex2bin(const char *hex, uint8_t *out, size_t max) +{ + size_t n = 0; + + if (!strcmp(hex, "-")) + return 0; + while (*hex && *(hex + 1) && n < max) { + unsigned v; + + if (sscanf(hex, "%2x", &v) != 1) + break; + out[n++] = (uint8_t)v; + hex += 2; + } + return n; +} + +int main(int argc, char **argv) +{ + FILE *f; + char line[8192]; + + if (argc != 2) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 1; + } + load_huk(); + + f = fopen(argv[1], "r"); + if (!f) { + fprintf(stderr, "cannot open %s\n", argv[1]); + return 1; + } + while (fgets(line, sizeof(line), f)) { + unsigned key_size, stack_size, ta_version; + char uuid_hex[64], extra_hex[4096]; + uint8_t uuid_bytes[16] = { 0 }; + /* + * Deliberately larger than the accepted maximum so that + * over-size extra data reaches the length check below intact + * rather than being silently truncated into a valid case. + */ + uint8_t extra[TA_DERIVED_EXTRA_DATA_MAX_SIZE + 64]; + size_t extra_len, stack_len, i; + uint8_t *stack; + TEE_Result res; + + if (line[0] == '#' || line[0] == '\n') + continue; + if (sscanf(line, "%u %u %u %63s %4095s", &key_size, &stack_size, + &ta_version, uuid_hex, extra_hex) != 5) + continue; + + hex2bin(uuid_hex, uuid_bytes, 16); + /* Raw struct bytes, exactly what the original memcpy'd. */ + memcpy(&g_uuid, uuid_bytes, 16); + if (strcmp(extra_hex, "-") && strlen(extra_hex) / 2 > sizeof(extra)) { + fprintf(stderr, "extra data too large for harness buffer\n"); + return 1; + } + extra_len = hex2bin(extra_hex, extra, sizeof(extra)); + g_ta_version = ta_version; + + stack_len = (size_t)key_size * ((size_t)ta_version + 1); + stack = calloc(stack_len ? stack_len : 1, 1); + if (!stack) + return 1; + + res = system_derive_ta_svn_key_stack(key_size, stack_size, extra, + extra_len, stack, stack_len); + printf("%08x ", res); + if (res == TEE_SUCCESS) + for (i = 0; i < stack_len; i++) + printf("%02x", stack[i]); + else + printf("-"); + printf("\n"); + free(stack); + } + fclose(f); + return 0; +}