diff --git a/Cargo.lock b/Cargo.lock index 647812e8d8..86f3d9eb6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1560,6 +1560,8 @@ dependencies = [ "litebox", "litebox_common_linux", "num_enum", + "rsa", + "sha2", "zerocopy", ] diff --git a/litebox_common_optee/Cargo.toml b/litebox_common_optee/Cargo.toml index 4e8b4d70dc..fcd904445e 100644 --- a/litebox_common_optee/Cargo.toml +++ b/litebox_common_optee/Cargo.toml @@ -9,7 +9,12 @@ elf = { version = "0.8.0", default-features = false } litebox = { path = "../litebox/", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } num_enum = { version = "0.7.3", default-features = false } +rsa = { version = "0.9.10", default-features = false, features = ["pem"], optional = true } +sha2 = { version = "0.10.9", default-features = false, features = ["oid"], optional = true } zerocopy = { version = "0.8", features = ["derive"] } +[features] +signed-ta-rsa = ["dep:rsa", "dep:sha2"] + [lints] workspace = true diff --git a/litebox_common_optee/src/lib.rs b/litebox_common_optee/src/lib.rs index 976e5ab26c..78852022fd 100644 --- a/litebox_common_optee/src/lib.rs +++ b/litebox_common_optee/src/lib.rs @@ -9,6 +9,8 @@ extern crate alloc; use alloc::boxed::Box; +#[cfg(feature = "signed-ta-rsa")] +use alloc::vec::Vec; use core::mem::size_of; use litebox::platform::RawConstPointer as _; use litebox::utils::TruncateExt; @@ -999,6 +1001,7 @@ impl TeeMemoryAccessRights { const TEE_ALG_AES_CTR: u32 = 0x1000_0210; const TEE_ALG_AES_GCM: u32 = 0x4000_0810; +const TEE_ALG_RSASSA_PKCS1_PSS_MGF1_SHA256: u32 = 0x7041_4930; const TEE_ALG_RSASSA_PKCS1_V1_5_SHA256: u32 = 0x7000_4830; const TEE_ALG_RSASSA_PKCS1_V1_5_SHA512: u32 = 0x7000_6830; const TEE_ALG_HMAC_SHA256: u32 = 0x3000_0004; @@ -1014,6 +1017,7 @@ const TEE_ALG_ILLEGAL_VALUE: u32 = 0xefff_ffff; pub enum TeeAlgorithm { AesCtr = TEE_ALG_AES_CTR, AesGcm = TEE_ALG_AES_GCM, + RsaPssSha256 = TEE_ALG_RSASSA_PKCS1_PSS_MGF1_SHA256, RsaPkcs1Sha256 = TEE_ALG_RSASSA_PKCS1_V1_5_SHA256, RsaPkcs1Sha512 = TEE_ALG_RSASSA_PKCS1_V1_5_SHA512, HmacSha256 = TEE_ALG_HMAC_SHA256, @@ -1055,9 +1059,9 @@ impl From for TeeAlgorithmClass { match algo { TeeAlgorithm::AesCtr | TeeAlgorithm::AesGcm => TeeAlgorithmClass::Cipher, TeeAlgorithm::HmacSha256 | TeeAlgorithm::HmacSha512 => TeeAlgorithmClass::Mac, - TeeAlgorithm::RsaPkcs1Sha256 | TeeAlgorithm::RsaPkcs1Sha512 => { - TeeAlgorithmClass::AsymmetricSignature - } + TeeAlgorithm::RsaPkcs1Sha256 + | TeeAlgorithm::RsaPkcs1Sha512 + | TeeAlgorithm::RsaPssSha256 => TeeAlgorithmClass::AsymmetricSignature, _ => TeeAlgorithmClass::Unknown, } } @@ -2646,3 +2650,133 @@ mod tests { assert!(OpteeRpcArgs::from_header_and_raw_params(&header, &[]).is_err()); } } + +/// `Shdr` from `optee_os/core/include/signed_hdr.h` +#[cfg(feature = "signed-ta-rsa")] +#[derive(Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Shdr { + pub magic: u32, + pub img_type: u32, + pub img_size: u32, + pub algo: u32, + pub hash_size: u16, + pub sig_size: u16, +} + +/// `SHDR_MAGIC` from `optee_os/core/include/signed_hdr.h` +#[cfg(feature = "signed-ta-rsa")] +const SHDR_MAGIC: u32 = 0x4f54_5348; + +/// From `optee_os/core/include/signed_hdr.h` +/// struct shdr_bootstrap_ta { +/// uint8_t uuid[sizeof(TEE_UUID)]; +/// uint32_t ta_version; +/// }; +/// and from `optee_os/core/include/tee_api_types.h` +/// typedef struct { +/// uint32_t timeLow; +/// uint16_t timeMid; +/// uint16_t timeHiAndVersion; +/// uint8_t clockSeqAndNode[8]; +/// } TEE_UUID; +#[cfg(feature = "signed-ta-rsa")] +const SHDR_UUID_LEN: usize = 16; +#[cfg(feature = "signed-ta-rsa")] +const SHDR_VERSION_LEN: usize = 4; + +/// An RSA public key used to verify signed `.ta` files +#[cfg(feature = "signed-ta-rsa")] +pub struct TaVerifyKey(rsa::RsaPublicKey); + +#[cfg(feature = "signed-ta-rsa")] +impl TaVerifyKey { + pub fn from_der(der: &[u8]) -> Result { + use rsa::pkcs8::DecodePublicKey; + + rsa::RsaPublicKey::from_public_key_der(der) + .map(TaVerifyKey) + .map_err(|_| "Invalid RSA public key DER") + } +} + +/// Parse and verify an OP-TEE signed .ta file. +/// A signed `.ta` file has the following layout: +/// shdr || hash || sig || uuid || ta_version || img +/// Return the parsed TaHead and the TA ELF binary. +#[cfg(feature = "signed-ta-rsa")] +pub fn parse_and_verify_ta<'a>( + ta_data: &'a [u8], + verify_key: &TaVerifyKey, +) -> Result<(TaHead, &'a [u8]), &'static str> { + let hdr_size = size_of::(); + if ta_data.len() < hdr_size { + return Err("Invalid signed TA file"); + } + let shdr = + Shdr::read_from_bytes(&ta_data[..hdr_size]).map_err(|_| "Invalid signed TA header")?; + if shdr.magic != SHDR_MAGIC { + return Err("Invalid TA magic"); + } + let hash_size = shdr.hash_size as usize; + let sig_size = shdr.sig_size as usize; + let img_size = shdr.img_size as usize; + + let hash_offset = hdr_size; + let sig_offset = hash_offset + .checked_add(hash_size) + .ok_or("Invalid signed TA file")?; + let uuid_offset = sig_offset + .checked_add(sig_size) + .ok_or("Invalid signed TA file")?; + let version_offset = uuid_offset + .checked_add(SHDR_UUID_LEN) + .ok_or("Invalid signed TA file")?; + let img_offset = version_offset + .checked_add(SHDR_VERSION_LEN) + .ok_or("Invalid signed TA file")?; + let end = img_offset + .checked_add(img_size) + .ok_or("Invalid signed TA file")?; + if end > ta_data.len() { + return Err("Invalid signed TA file"); + } + let sig = &ta_data[sig_offset..uuid_offset]; + let uuid_and_version = &ta_data[uuid_offset..img_offset]; + let img = &ta_data[img_offset..end]; + + // The signed message is shdr || uuid || ta_version || img. + let mut signed_message = Vec::with_capacity(hdr_size + uuid_and_version.len() + img.len()); + signed_message.extend_from_slice(&ta_data[..hdr_size]); + signed_message.extend_from_slice(uuid_and_version); + signed_message.extend_from_slice(img); + verify_shdr_signature(&signed_message, sig, shdr.algo, &verify_key.0)?; + let ta_head = parse_ta_head(img).ok_or("Invalid TA ELF binary")?; + + Ok((ta_head, img)) +} + +/// Verify a signed `.ta` file's signature against the given RSA public key. +/// TODO: Support more signature algorithms if needed. +#[cfg(feature = "signed-ta-rsa")] +fn verify_shdr_signature( + message: &[u8], + signature: &[u8], + algo: u32, + rsa_pub_key: &rsa::RsaPublicKey, +) -> Result<(), &'static str> { + use rsa::signature::Verifier; + use sha2::Sha256; + + match algo { + TEE_ALG_RSASSA_PKCS1_PSS_MGF1_SHA256 => { + let verifying_key = rsa::pss::VerifyingKey::::new(rsa_pub_key.clone()); + let sig = + rsa::pss::Signature::try_from(signature).map_err(|_| "Invalid PSS signature")?; + verifying_key + .verify(message, &sig) + .map_err(|_| "Signature verification failed") + } + _ => Err("Unsupported TA signature algorithm"), + } +} diff --git a/litebox_runner_lvbs/Cargo.toml b/litebox_runner_lvbs/Cargo.toml index 5ad183ec92..baa29f8e40 100644 --- a/litebox_runner_lvbs/Cargo.toml +++ b/litebox_runner_lvbs/Cargo.toml @@ -23,6 +23,7 @@ x86_64 = { version = "0.15.2", default-features = false, features = ["instructio [features] devbox = ["litebox_platform_lvbs/devbox"] preemption_test_quantum = ["litebox_platform_lvbs/preemption_test_quantum"] +signed-ta-rsa = ["litebox_shim_optee/signed-ta-rsa"] [lints] workspace = true diff --git a/litebox_runner_lvbs/src/lib.rs b/litebox_runner_lvbs/src/lib.rs index 442eba6ebc..51b8a4de65 100644 --- a/litebox_runner_lvbs/src/lib.rs +++ b/litebox_runner_lvbs/src/lib.rs @@ -824,13 +824,14 @@ fn open_session_new_instance( ta_req_info: &litebox_shim_optee::msg_handler::TaRequestInfo, ) -> Result<(), OpteeSmcReturnCode> { let shim = litebox_shim_optee::OpteeShimBuilder::new(platform, session_manager()).build(); - if shim.get_ta_bin(&ta_uuid).is_none() { + let Some(ta_source) = shim.get_ta_source(&ta_uuid) else { msg_args.session = 0; msg_args.ret = TeeResult::ItemNotFound; msg_args.ret_origin = TeeOrigin::Tee; write_non_ta_msg_args_to_normal_world(platform, msg_args, msg_args_phys_addr)?; return Ok(()); - } + }; + debug_serial_println!("Loading TA: uuid={:?}, source={:?}", ta_uuid, ta_source); // Token is declared before `task_pt_guard` so it drops AFTER it. // Marker only releases once CR3 is back to base. See @@ -1414,15 +1415,12 @@ const LDELF_BINARY: &[u8] = &[0u8; 0]; const TA_BINARY: &[u8] = &[0u8; 0]; const TA_BINARIES: &[&[u8]] = &[TA_BINARY]; -/// Register a TA binary embedded in the runner image. +/// Register a raw or signed TA binary embedded in the runner image. fn register_embedded_ta( shim: &litebox_shim_optee::OpteeShim, ta_binary: &'static [u8], ) -> bool { - let Some(ta_head) = litebox_common_optee::parse_ta_head(ta_binary) else { - return false; - }; - shim.store_ta_bin(&ta_head.uuid, ta_binary) + shim.store_embedded_ta(ta_binary) } /// Register all TA binaries embedded in the runner image. diff --git a/litebox_runner_optee_on_linux_userland/src/lib.rs b/litebox_runner_optee_on_linux_userland/src/lib.rs index f70ecbdde7..fbfd1a366a 100644 --- a/litebox_runner_optee_on_linux_userland/src/lib.rs +++ b/litebox_runner_optee_on_linux_userland/src/lib.rs @@ -114,7 +114,7 @@ fn run_ta_with_default_commands( let ta_uuid = litebox_common_optee::parse_ta_head(ta_bin) .expect("Failed to parse TA header from ta_bin") .uuid; - assert!(shim.store_ta_bin(&ta_uuid, ta_bin)); + assert!(shim.store_ta_bin(&ta_uuid, ta_bin, litebox_shim_optee::TaSource::BuiltIn,)); for func_id in [UteeEntryFunc::OpenSession, UteeEntryFunc::CloseSession] { let params = [const { UteeParamOwned::None }; UteeParamOwned::TEE_NUM_PARAMS]; diff --git a/litebox_runner_optee_on_linux_userland/src/tests.rs b/litebox_runner_optee_on_linux_userland/src/tests.rs index e5c6ec8bf7..548e173586 100644 --- a/litebox_runner_optee_on_linux_userland/src/tests.rs +++ b/litebox_runner_optee_on_linux_userland/src/tests.rs @@ -29,7 +29,7 @@ pub fn run_ta_with_test_commands( }; let ta_head = litebox_common_optee::parse_ta_head(ta_bin).expect("Failed to parse TA header from ta_bin"); - assert!(shim.store_ta_bin(&ta_head.uuid, ta_bin)); + assert!(shim.store_ta_bin(&ta_head.uuid, ta_bin, litebox_shim_optee::TaSource::BuiltIn,)); let mut ta_info: Option> = None; // The active session id for the TA. Set at OpenSession and reused for the // subsequent InvokeCommand entries on the same persistent session. diff --git a/litebox_shim_optee/Cargo.toml b/litebox_shim_optee/Cargo.toml index 75a74e9038..97f7cc7abb 100644 --- a/litebox_shim_optee/Cargo.toml +++ b/litebox_shim_optee/Cargo.toml @@ -23,6 +23,9 @@ zerocopy = { version = "0.8", default-features = false, features = ["derive"] } zeroize = { version = "1.8", default-features = false, features = ["alloc"] } p384 = { version = "0.13.1", default-features = false, features = ["arithmetic", "ecdsa"] } +[features] +signed-ta-rsa = ["litebox_common_optee/signed-ta-rsa"] + [lints] workspace = true diff --git a/litebox_shim_optee/src/lib.rs b/litebox_shim_optee/src/lib.rs index 4d96ccd24b..3bbe551b69 100644 --- a/litebox_shim_optee/src/lib.rs +++ b/litebox_shim_optee/src/lib.rs @@ -216,12 +216,59 @@ struct GlobalState { } impl GlobalState { + /// Store a trusted TA embedded in the runner image. + /// + /// Raw ELF binaries are stored directly. Signed TAs are verified and + /// unwrapped when `signed-ta-rsa` is enabled. + pub(crate) fn store_embedded_ta(&self, ta_bin: &[u8]) -> bool { + if let Some(ta_head) = litebox_common_optee::parse_ta_head(ta_bin) { + return self + .ta_uuid_map + .insert(ta_head.uuid, ta_bin.into(), TaSource::BuiltIn); + } + + #[cfg(feature = "signed-ta-rsa")] + { + let Some((ta_uuid, ta_elf)) = verify_signed_ta(ta_bin) else { + return false; + }; + self.ta_uuid_map + .insert(ta_uuid, ta_elf.into(), TaSource::BuiltIn) + } + #[cfg(not(feature = "signed-ta-rsa"))] + { + false + } + } + /// Store the TA binary associated with the given TA UUID. /// + /// Built-in binaries are trusted raw ELF files. Dynamic binaries must be signed + /// and are verified before their inner ELF is cached. + /// /// Returns `true` if the binary was successfully stored, `false` if the binary's /// UUID (from `.ta_head` section) doesn't match the provided UUID or parsing failed. - pub(crate) fn store_ta_bin(&self, ta_uuid: &TeeUuid, ta_bin: &[u8]) -> bool { - self.ta_uuid_map.insert(*ta_uuid, ta_bin.into()) + pub(crate) fn store_ta_bin(&self, ta_uuid: &TeeUuid, ta_bin: &[u8], source: TaSource) -> bool { + let ta_elf = match source { + TaSource::BuiltIn => ta_bin, + TaSource::Dynamic => { + #[cfg(feature = "signed-ta-rsa")] + { + let Some((verified_uuid, ta_elf)) = verify_signed_ta(ta_bin) else { + return false; + }; + if verified_uuid != *ta_uuid { + return false; + } + ta_elf + } + #[cfg(not(feature = "signed-ta-rsa"))] + { + return false; + } + } + }; + self.ta_uuid_map.insert(*ta_uuid, ta_elf.into(), source) } /// Get the TA binary associated with the given TA UUID. @@ -230,10 +277,10 @@ impl GlobalState { Some(ta_bin) } else { let ta_bin = Self::rpc_get_ta_bin(ta_uuid)?; - if !self.store_ta_bin(ta_uuid, &ta_bin) { + if !self.store_ta_bin(ta_uuid, &ta_bin, TaSource::Dynamic) { return None; } - Some(ta_bin) + self.ta_uuid_map.get(ta_uuid) } } @@ -242,6 +289,11 @@ impl GlobalState { self.ta_uuid_map.get_flags(ta_uuid).unwrap_or_default() } + /// Get how the cached TA binary was loaded. + pub(crate) fn get_ta_source(&self, ta_uuid: &TeeUuid) -> Option { + self.ta_uuid_map.get_source(ta_uuid) + } + /// Monotonic time elapsed since this instance was created, used as GP /// "system time" (`TEE_GetSystemTime`). /// @@ -360,8 +412,13 @@ impl OpteeShim { /// /// Returns `true` if the binary was successfully stored, `false` if the binary's /// UUID (from `.ta_head` section) doesn't match the provided UUID or parsing failed. - pub fn store_ta_bin(&self, ta_uuid: &TeeUuid, ta_bin: &[u8]) -> bool { - self.0.store_ta_bin(ta_uuid, ta_bin) + pub fn store_ta_bin(&self, ta_uuid: &TeeUuid, ta_bin: &[u8], source: TaSource) -> bool { + self.0.store_ta_bin(ta_uuid, ta_bin, source) + } + + /// Store a raw or signed TA embedded in the runner image. + pub fn store_embedded_ta(&self, ta_bin: &[u8]) -> bool { + self.0.store_embedded_ta(ta_bin) } /// Get the TA binary associated with the given TA UUID. @@ -369,6 +426,11 @@ impl OpteeShim { self.0.get_ta_bin(ta_uuid) } + /// Get how the cached TA binary was loaded. + pub fn get_ta_source(&self, ta_uuid: &TeeUuid) -> Option { + self.0.get_ta_source(ta_uuid) + } + /// Release all user-space memory mappings owned by this shim instance. /// /// This must be called before switching to the base page table and deleting @@ -1401,12 +1463,21 @@ impl TaHandleMap { } } +/// Whether the TA binary was built into the runner or dynamically loaded at runtime. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TaSource { + BuiltIn, + Dynamic, +} + /// Entry in the TA UUID map containing binary data and parsed flags. struct TaInfo { /// The raw TA binary binary: Arc<[u8]>, /// Parsed TA flags from .ta_head section flags: TaFlags, + /// How the TA binary was loaded + source: TaSource, } /// Data structure to maintain a mapping from TA UUIDs to their binary data and flags. @@ -1421,7 +1492,7 @@ impl TaUuidMap { } } - pub(crate) fn insert(&self, uuid: TeeUuid, ta_bin: Arc<[u8]>) -> bool { + pub(crate) fn insert(&self, uuid: TeeUuid, ta_bin: Arc<[u8]>, source: TaSource) -> bool { // Parse TA head from the binary's .ta_head section let Some(ta_head) = litebox_common_optee::parse_ta_head(&ta_bin) else { return false; @@ -1437,6 +1508,7 @@ impl TaUuidMap { TaInfo { binary: ta_bin, flags: ta_head.flags, + source, }, ); true @@ -1451,6 +1523,11 @@ impl TaUuidMap { self.inner.read().get(uuid).map(|info| info.flags) } + /// Get how the TA binary for a given UUID entered the cache. + pub(crate) fn get_source(&self, uuid: &TeeUuid) -> Option { + self.inner.read().get(uuid).map(|info| info.source) + } + // Lazy removal of TA binaries when they are no longer needed. pub(crate) fn remove(&self, uuid: &TeeUuid) -> Option> { self.inner.write().remove(uuid).map(|info| info.binary) @@ -1463,6 +1540,16 @@ fn ta_uuid_map() -> &'static TaUuidMap { TA_UUID_MAP.get_or_init(|| alloc::boxed::Box::new(TaUuidMap::new())) } +#[cfg(feature = "signed-ta-rsa")] +fn verify_signed_ta(ta_bin: &[u8]) -> Option<(TeeUuid, &[u8])> { + use litebox_common_optee::{TaVerifyKey, parse_and_verify_ta}; + + const TA_VERIFY_KEY_DER: &[u8] = include_bytes!(env!("LITEBOX_TA_VERIFY_KEY")); + let verify_key = TaVerifyKey::from_der(TA_VERIFY_KEY_DER).ok()?; + let (ta_head, ta_elf) = parse_and_verify_ta(ta_bin, &verify_key).ok()?; + Some((ta_head.uuid, ta_elf)) +} + /// Per-instance TA state which can be shared between sessions if it is /// a single-instance multi-session TA. The active session id is carried /// per entry (see [`Task::current_session_id`]).