Consortium TEE: Secure Execution Environment
The Consortium TEE provides a type-safe, macro-driven interface for building OP-TEE Trusted Applications (TAs) and calling them securely from the normal world (A-core Linux application).
TEE Architecture
┌──────────────────────────────────────────────────────────────┐
│ A-Core (Normal World - Linux) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Consortium Application │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ CA (Client Application) │ │ │
│ │ │ call_authenticate(secret) │ │ │
│ │ │ call_encrypt(plaintext) → ciphertext │ │ │
│ │ │ call_derive_key(seed) │ │ │
│ │ │ │ │ │
│ │ │ (generated by #[tee_command] macro) │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └──────────────┬───────────────────────────────────────────┘ │
│ │ optee_teec (TEE Client API) │
│ │ UUID, session, invoke_command │
│ ▼ │
│ OP-TEE Kernel Driver │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────┬──────────────────────────────────────────┘
│ SMC (Secure Monitor Call)
┌────────────────────▼──────────────────────────────────────────┐
│ Secure World (OP-TEE OS) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Trusted Application (TA) │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ TA Command Handlers (generated) │ │ │
│ │ │ fn authenticate_dispatched(ctx, params) │ │ │
│ │ │ fn encrypt_dispatched(ctx, params) │ │ │
│ │ │ fn derive_key_dispatched(ctx, params) │ │ │
│ │ │ │ │ │
│ │ │ Unpacks parameters, calls user logic │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ User-Defined Business Logic │ │ │
│ │ │ fn authenticate(ctx, secret) → bool │ │ │
│ │ │ fn encrypt(ctx, plaintext) → Vec<u8> │ │ │
│ │ │ fn derive_key(ctx, seed) → [u8; 32] │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Hardware Crypto Engine (AES, SHA, RSA, etc.) │
│ (optee_utee provides safe wrappers) │
└───────────────────────────────────────────────────────────────┘
Developer Experience: Define Once, Generate Twice
Developers write a single Rust function and the #[tee_command] macro generates both TA and CA code automatically.
Step 1: Define the Trusted Application (Shared Library)
#![allow(unused)]
fn main() {
// my-ta/src/lib.rs
use consortium_tee_macros::{tee_command, tee_service};
// Define context (shared state)
pub struct TaContext {
counter: u32,
hmac_key: [u8; 32],
}
// Define service with commands
tee_service! {
context TaContext
commands {
authenticate,
encrypt_aes_cbc,
derive_key_hmac,
}
}
// Business logic: just plain Rust
#[tee_command(ctx)]
fn authenticate(ctx: &mut TaContext, secret: &[u8]) -> Result<bool> {
ctx.counter += 1;
Ok(constant_time_compare(secret, &ctx.hmac_key))
}
#[tee_command(ctx)]
fn encrypt_aes_cbc(
ctx: &mut TaContext,
plaintext: &[u8],
ciphertext: &mut [u8],
) -> Result<u32> {
// ciphertext is pre-allocated by CA
let iv = &ctx.hmac_key[0..16];
let key = &ctx.hmac_key[16..32];
// Use optee_utee::crypto (secure, never copied to normal world)
optee_utee::crypto::aes_cbc_encrypt(key, iv, plaintext, ciphertext)?;
Ok(ciphertext.len() as u32)
}
#[tee_command(ctx)]
fn derive_key_hmac(ctx: &mut TaContext, seed: &[u8]) -> Result<Vec<u8>> {
// Vec<u8> return: CA pre-allocates buffer, TA writes into it
let mut key = vec![0u8; 32];
optee_utee::crypto::hmac_sha256(seed, &ctx.hmac_key, &mut key)?;
Ok(key)
}
}
Step 2: TA Binary (with Generated Dispatcher)
#![allow(unused)]
fn main() {
// my-ta/src/main.rs
#![no_main]
#![no_std]
use my_ta::*;
// Generated by macro: invoke_command(ctx, cmd_id, params)
#[no_mangle]
extern "C" fn TA_InvokeCommand(
sess_ctx: *mut c_void,
cmd_id: u32,
param_types: u32,
params: *mut TEE_Param,
) -> TEE_Result {
// Dispatcher generated by tee_service! macro
invoke_command(
&mut TA_CONTEXT,
cmd_id,
param_types,
unsafe { &mut *params.cast() },
)
}
// Generated TA header by build.rs
include!(concat!(env!("OUT_DIR"), "/user_ta_header.rs"));
}
What the macro generates:
invoke_command()dispatcher that matchescmd_idto function handlers- For each
#[tee_command]function, afunction_name_dispatched()handler that:- Unpacks parameters from TEE slots (ParamValue, ParamMemref)
- Validates parameter types
- Calls the user’s function
- Packs return values back into output slots
Step 3: CA Client Application (with Generated Stubs)
// my-ca/src/main.rs
use my_ta::call_authenticate;
use my_ta::call_encrypt_aes_cbc;
use my_ta::call_derive_key_hmac;
#[tokio::main]
async fn main() -> Result<()> {
// Generated stubs handle all TEE protocol machinery
// Call 1: authenticate
let secret = b"super-secret-password";
let is_valid: bool = call_authenticate(secret).await?;
println!("Authentication: {}", is_valid);
// Call 2: encrypt
let plaintext = b"Hello, Secure World!";
let mut ciphertext = vec![0u8; plaintext.len() + 16]; // AES padding
let encrypted_len = call_encrypt_aes_cbc(plaintext, &mut ciphertext).await?;
ciphertext.truncate(encrypted_len as usize);
println!("Encrypted: {:x?}", ciphertext);
// Call 3: derive key
let seed = b"random-seed-12345";
let derived_key: Vec<u8> = call_derive_key_hmac(seed).await?;
println!("Derived key: {:x?}", derived_key);
Ok(())
}
What the macro generates:
- For each
#[tee_command]function, acall_function_name()stub that:- Creates a TEE session (if not already open)
- Packs parameters into ParamValue / ParamMemref structures
- Invokes the TA via
optee_teec::Session::invoke_command() - Unpacks return values
- Returns as a native Rust type
TEE Type System
The #[tee_command] macro supports advanced type conversion:
#![allow(unused)]
fn main() {
#[derive(TeeParam, Serialize, Deserialize)]
struct Config {
threshold: u32,
enabled: bool,
}
#[tee_command(codec = PostcardCodec)] // ← specify codec for TeeParam types
fn process(
ctx: &mut TaContext,
// Input types (TEE slots → Rust types)
flag: bool, // ParamValue.a()
count: u32, // ParamValue.a()
data: &[u8], // ParamMemref.buffer()
output: &mut [u8], // ParamMemref (mutable)
config: Config, // TeeParam (deserialized via codec)
// Output types (Rust types → TEE slots)
) -> Result<Vec<u8>> {
// User logic
Ok(data.to_vec())
}
}
Mapping:
| Rust Type | TEE Slot | Direction | Notes |
|---|---|---|---|
bool | ParamValue.a (u32) | in/out | |
u32, i32 | ParamValue.a | in/out | |
u64 | ParamValue (a + b) | in/out | split across a(low) and b(high) |
&[u8] | ParamMemref (read-only) | in | |
&mut [u8] | ParamMemref (read-write) | in/out | |
T: TeeParam | ParamMemref | in | deserialized via codec |
&mut T: TeeParam | ParamMemref | in/out | round-trip via codec |
Vec<u8> | ParamMemref (output buffer) | out | |
T: TeeParam | ParamMemref | out | serialized via codec |
For TeeParam types, the #[tee_command(codec = ...)] attribute is required. See tee-advanced-type-system.md.
TEE Performance & Security
Latency:
- CA → TA call: ~100–500 µs (SMC + OP-TEE context switch)
- Crypto operation (SHA256): ~1–10 ms
- Total roundtrip: dominated by crypto, not IPC overhead
Memory:
- TA binary: ~100–500 KB
- CA shared memory buffers: 4 KB per call (for large payloads)
Security Guarantees:
- Data in TA never leaves secure world in plaintext
- Crypto keys isolated from normal world OS
- TA isolation enforced by hardware (TrustZone)
- Type safety prevents buffer overflow exploits in CA ↔ TA boundary
Build Integration
# my-ta/Cargo.toml
[lib]
crate-type = ["rlib"]
[[bin]]
name = "ta"
crate-type = ["cdylib"]
[dependencies]
optee_utee = "0.4"
consortium-tee = { path = "../../consortium-tee" }
consortium-tee-macros = { path = "../../consortium-tee-macros" }
consortium-cfg-common = { path = "../../consortium-cfg-common" }
# Optional: add only if using TeeParam types
# Example: if using PostcardCodec for serialization
serde = { version = "1.0", features = ["derive"], optional = true }
postcard = { version = "1.0", optional = true }
[build-dependencies]
optee_utee_build = "0.4"
[features]
ca = [] # CA-side code generation (enabled by CA's build)
postcard-codec = ["serde", "dep:postcard"] # Enable postcard codec support
Build process:
build.rsrunsoptee_utee_build::build()→ generatesuser_ta_header.rs- Macro generates
invoke_command()dispatcher cdylibcompiles to TA binary- CA depends on
rlibwithfeatures = ["ca"]→ generatescall_*stubs