Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 matches cmd_id to function handlers
  • For each #[tee_command] function, a function_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, a call_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 TypeTEE SlotDirectionNotes
boolParamValue.a (u32)in/out
u32, i32ParamValue.ain/out
u64ParamValue (a + b)in/outsplit across a(low) and b(high)
&[u8]ParamMemref (read-only)in
&mut [u8]ParamMemref (read-write)in/out
T: TeeParamParamMemrefindeserialized via codec
&mut T: TeeParamParamMemrefin/outround-trip via codec
Vec<u8>ParamMemref (output buffer)out
T: TeeParamParamMemrefoutserialized 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:

  1. build.rs runs optee_utee_build::build() → generates user_ta_header.rs
  2. Macro generates invoke_command() dispatcher
  3. cdylib compiles to TA binary
  4. CA depends on rlib with features = ["ca"] → generates call_* stubs