A Cargo subcommand for building OP-TEE Trusted Applications (TAs) and Client Applications (CAs) in Rust.
cargo-optee simplifies the development workflow for OP-TEE applications by replacing complex Makefiles with a unified, type-safe command-line interface. It handles cross-compilation, custom target specifications, environment setup, and signing automatically.
┌──────────────────┐
│ TA Developer │
│ (CLI input) │
└────────┬─────────┘
│
▼
┌──────────────────────────────────────────────┐
│ cargo-optee (this tool) │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ 1. Parse CLI & Validate Parameters │ │
│ │ - Architecture (aarch64/arm) │ │
│ │ - Build mode (std/no-std) │ │
│ │ - Build type (TA/CA/PLUGIN) │ │
│ └──────────────────┬─────────────────────┘ │
│ │ │
│ ┌──────────────────▼─────────────────────┐ │
│ │ 2. Setup Build Environment │ │
│ │ - Set environment variables │ │
│ │ - Configure cross-compiler │ │
│ └──────────────────┬─────────────────────┘ │
│ │ │
│ ┌──────────────────▼─────────────────────┐ │
│ │ 3. Execute Build Pipeline │ │
│ │ - Run clippy (linting) │ │
│ │ - Build binary: cargo + gcc │ │
│ │ - Strip symbols: objcopy │ │
│ │ - Sign TA: Python script (TA only) │ │
│ └──────────────────┬─────────────────────┘ │
│ │ │
└─────────────────────┼────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Low-Level Tools (dependencies) │
│ │
│ - cargo: Rust compilation │
│ - gcc: Linking with OP-TEE libraries │
│ - objcopy: Symbol stripping │
│ - Python script: TA signing (TA only) │
│ │
└──────────────────────────────────────────────┘
Assume developers have Rust, Cargo, and the gcc toolchain installed and added to PATH (the guide is in future plan). Then install cargo-optee using Cargo:
cargo install cargo-optee
This section provides a quick start guide for building the Hello World example using cargo-optee. Before proceeding, ensure you have set up the Docker development environment. For detailed instructions on setting up the Docker environment, refer to the QEMU emulation guide.
First, pull and run the Docker image. We provide two types of images:
Pull and start the no-std development environment:
# Pull the pre-built development environment image docker pull teaclave/teaclave-trustzone-emulator-nostd-expand-memory:latest # Start the development container docker run -it --rm \ --name teaclave_dev_env \ -v $(pwd):/root/teaclave_sdk_src \ -w /root/teaclave_sdk_src \ teaclave/teaclave-trustzone-emulator-nostd-expand-memory:latest
📖 Note: If you need Rust standard library (std) support, refer to the Docker development environment guide with std support and use the
teaclave/teaclave-trustzone-emulator-std-expand-memory:latestimage.
1. Navigate to the Hello World example directory
Inside the Docker container, execute:
cd examples/hello_world-rs/
2. Build the Trusted Application (TA)
Build the TA using cargo-optee. In the Docker environment, the OP-TEE TA development kit is typically located at /opt/teaclave/optee/optee_os/out/arm-plat-vexpress/export-ta_arm64:
# Build aarch64 no-std TA (default configuration) cargo-optee build ta \ --manifest-path ta/Cargo.toml \ --ta-dev-kit-dir /opt/teaclave/optee/optee_os/out/arm-plat-vexpress/export-ta_arm64 \ --arch aarch64 \ --no-std
3. Build the Client Application (CA)
Build the client application:
# Build aarch64 CA cargo-optee build ca \ --manifest-path host/Cargo.toml \ --optee-client-export /opt/teaclave/optee/optee_client/export_arm64 \ --arch aarch64
💡 Tip: If you configure metadata in your
Cargo.tomlfile (see Configuration System), you can simplify the command by only specifying--manifest-path:cargo-optee build ca --manifest-path host/Cargo.toml
After a successful build, you can find the generated files at the following locations:
ta/target/aarch64-unknown-linux-gnu/release/133af0ca-bdab-11eb-9130-43bf7873bf67.tahost/target/aarch64-unknown-linux-gnu/release/hello_world-rsAfter building, you can:
sync_to_emulator command to sync build artifacts to the emulator environment💡 Tip: If you configure metadata in
Cargo.toml(see Configuration System), you can simplify build commands by only specifying--manifest-path.
cargo-optee uses a flexible configuration system with the following priority (highest to lowest):
[package.metadata.optee.*] sections (see Build through metadata)This allows projects to define their standard configuration in Cargo.toml while still permitting CLI overrides for specific builds.
Cargo-optee expects the following project structure by default.
project/
├── uuid.txt # TA UUID
├── ta/ # Trusted Application
│ ├── Cargo.toml
│ ├── src/
│ │ └── main.rs
│ └── build.rs # Build script
├── host/ # Client Application (host)
│ ├── Cargo.toml
│ ├── src/
│ │ └── main.rs
└── proto/ # Shared definitions such as TA command IDs and TA UUID
├── Cargo.toml
└── src/
└── lib.rs
See examples in the SDK for reference, such as hello_world-rs. The cargo new command (planned, not yet available) will generate a project template with this structure. For now, copy an existing example as a starting point.
For development and emulation, developers would like to build the one project and deploy to a target filesystem (e.g. QEMU shared folder) quickly. Frequent builds and quick rebuilds are common.
Using CLI arguments:
# 1. Create new project (future) cargo-optee new my_app cd my_app # 2. Build TA and CA cargo-optee build ta \ --ta-dev-kit-dir $TA_DEV_KIT_DIR \ --manifest-path ./ta/Cargo.toml \ --arch aarch64 \ --std cargo-optee build ca \ --optee-client-export $OPTEE_CLIENT_EXPORT \ --manifest-path ./host/Cargo.toml \ --arch aarch64 # 3. Install to specific folder (future), e.g. QEMU shared folder for emulation cargo-optee install --target /tmp/qemu-shared-folder
Using metadata configuration:
# 1. Configure once in Cargo.toml files, then simple builds cd ta && cargo-optee build ta cd ../host && cargo-optee build ca # 2. Override specific parameters when needed cd ta && cargo-optee build ta --debug # Override to debug build cd host && cargo-optee build ca --arch arm # Override architecture
For production and CI environments, artifacts should be cleaned up after successful builds. It can help to avoid storage issues on CI runners.
Automated Build Pipeline:
#!/bin/bash # CI build script set -e # Build TA (release mode) cargo-optee build ta \ --ta-dev-kit-dir $TA_DEV_KIT_DIR \ --manifest-path ./ta/Cargo.toml \ --arch aarch64 \ --std \ --signing-key ./keys/production.pem # Build CA (release mode) cargo-optee build ca \ --optee-client-export $OPTEE_CLIENT_EXPORT \ --manifest-path ./host/Cargo.toml \ --arch aarch64 # Install to staging area (future) cargo-optee install --target ./dist # Clean build artifacts to save space (future) cargo-optee clean --all
cargo-optee build ta \ --ta-dev-kit-dir <PATH> \ [--manifest-path <PATH>] \ [--arch aarch64|arm] \ [--std] \ [--no-std] \ [--signing-key <PATH>] \ [--uuid-path <PATH>] \ [--debug]
Required:
--ta-dev-kit-dir <PATH>: Path to OP-TEE TA development kit (available after building OP-TEE OS), user must provide this for building TAs.Optional:
--manifest-path <PATH>: Path to Cargo.toml manifest file--arch <ARCH>: Target architecture (default: aarch64)aarch64: ARM 64-bit architecturearm: ARM 32-bit architecture--std: Build with std support (uses cargo -Z build-std and custom target)--no-std: Build without std support (mutually exclusive with --std)--signing-key <PATH>: Path to signing key (default: <ta-dev-kit-dir>/keys/default_ta.pem)--uuid-path <PATH>: Path to UUID file (default: ../uuid.txt)--debug: Build in debug mode (default: release mode)Example:
# Build aarch64 TA with std support cargo-optee build ta \ --ta-dev-kit-dir /opt/optee/export-ta_arm64 \ --manifest-path ./examples/ta/hello_world-rs/Cargo.toml \ --arch aarch64 \ --std # Build arm TA without std (no-std) cargo-optee build ta \ --ta-dev-kit-dir /opt/optee/export-ta_arm32 \ --manifest-path ./ta/Cargo.toml \ --arch arm --no-std # Build TA with Cargo.toml metadata configuration # Note: ta-dev-kit-dir must be configured in Cargo.toml for this work cargo-optee build ta \ --manifest-path ./ta/Cargo.toml
Output:
target/<target-triple>/release/<uuid>.tatarget/ directorycargo-optee build ca \ --optee-client-export <PATH> \ [--manifest-path <PATH>] \ [--arch aarch64|arm] \ [--debug]
Required:
--optee-client-export <PATH>: Path to OP-TEE client library directory (available after building OP-TEE client), user must provide this for building CAs.Optional:
--manifest-path <PATH>: Path to Cargo.toml manifest file--arch <ARCH>: Target architecture (default: aarch64)--debug: Build in debug mode (default: release mode)Example:
# Build aarch64 client application cargo-optee build ca \ --optee-client-export /opt/optee/export-client \ --manifest-path ./examples/ca/hello_world-rs/Cargo.toml \ --arch aarch64
Output:
target/<target-triple>/release/<binary-name>We have one example for plugin: examples/ca/supp_plugin-rs-plugin.
cargo-optee build plugin \ --optee-client-export <PATH> \ --uuid-path <PATH> \ [--manifest-path <PATH>] \ [--arch aarch64|arm] \ [--debug]
Required:
--optee-client-export <PATH>: Path to OP-TEE client library directory (available after building OP-TEE client), user must provide this for building plugins.--uuid-path <PATH>: Path to UUID file for naming the pluginOptional:
--manifest-path <PATH>: Path to Cargo.toml manifest file--arch <ARCH>: Target architecture (default: aarch64)--debug: Build in debug mode (default: release mode)Example:
# Build aarch64 plugin cargo-optee build plugin \ --optee-client-export /opt/optee/export-client \ --manifest-path ./examples/ca/supp_plugin-rs-plugin/Cargo.toml \ --uuid-path ./examples/ca/supp_plugin-rs-plugin/plugin_uuid.txt \ --arch aarch64
Output:
target/<target-triple>/release/<uuid>.plugin.soConfigure TA builds in your Cargo.toml:
[package.metadata.optee.ta] arch = "aarch64" # Target architecture: "aarch64" | "arm" (optional, default: "aarch64") debug = false # Debug build: true | false (optional, default: false) std = false # Use std library: true | false (optional, default: false) uuid-path = "../uuid.txt" # Path to UUID file (optional, default: "../uuid.txt") # Architecture-specific configuration (omitted architectures default to null/unsupported) ta-dev-kit-dir = { aarch64 = "/opt/optee/export-ta_arm64", arm = "/opt/optee/export-ta_arm32" } signing-key = "/path/to/key.pem" # Path to signing key (optional, defaults to ta-dev-kit/keys/default_ta.pem)
Allowed entries:
arch: Target architecture ("aarch64" or "arm")debug: Build in debug mode (true or false)std: Enable std library support (true or false)uuid-path: Relative or absolute path to UUID fileta-dev-kit-dir: Architecture-specific paths to TA development kit (required)signing-key: Path to signing key fileConfigure CA builds in your Cargo.toml:
[package.metadata.optee.ca] arch = "aarch64" # Target architecture: "aarch64" | "arm" (optional, default: "aarch64") debug = false # Debug build: true | false (optional, default: false) # Architecture-specific configuration # if your CA only supports aarch64, you can omit arm optee-client-export = { aarch64 = "/opt/optee/export-client_arm64" }
Allowed entries:
arch: Target architecture ("aarch64" or "arm")debug: Build in debug mode (true or false)optee-client-export: Architecture-specific paths to OP-TEE client export (required)Configure plugin builds in your Cargo.toml:
[package.metadata.optee.plugin] arch = "aarch64" # Target architecture: "aarch64" | "arm" (optional, default: "aarch64") debug = false # Debug build: true | false (optional, default: false) uuid-path = "../plugin_uuid.txt" # Path to UUID file (required for plugins) # Architecture-specific configuration optee-client-export = { aarch64 = "/opt/optee/export-client_arm64", arm = "/opt/optee/export-client_arm32" }
Allowed entries:
arch: Target architecture ("aarch64" or "arm")debug: Build in debug mode (true or false)uuid-path: Relative or absolute path to UUID file (required for plugins)optee-client-export: Architecture-specific paths to OP-TEE client export (required)| Feature | Status | Notes |
|---|---|---|
build ta | ✅ Implemented | Supports aarch64/arm, std/no-std |
build ca | ✅ Implemented | Supports aarch64/arm |
build plugin | ✅ Implemented | Supports aarch64/arm, builds shared library plugins |
clean | ✅ Implemented | Remove build artifacts |
new | ⏳ Planned | Project scaffolding |
install | ⏳ Planned | Deploy to target filesystem |
User Input:
cargo-optee build ta \ --ta-dev-kit-dir /opt/optee/export-ta_arm64 \ --manifest-path ./ta/Cargo.toml \ --arch aarch64
cargo-optee translates to:
# 1. Clippy cd ./ta TA_DEV_KIT_DIR=/opt/optee/export-ta_arm64 \ RUSTFLAGS="-C panic=abort" \ cargo clippy --target aarch64-unknown-linux-gnu --release # 2. Build TA_DEV_KIT_DIR=/opt/optee/export-ta_arm64 \ RUSTFLAGS="-C panic=abort" \ cargo build --target aarch64-unknown-linux-gnu --release \ --manifest-path ./ta/Cargo.toml \ --config target.aarch64-unknown-linux-gnu.linker="aarch64-linux-gnu-gcc" # 3. Strip aarch64-linux-gnu-objcopy --strip-unneeded \ target/aarch64-unknown-linux-gnu/release/ta \ target/aarch64-unknown-linux-gnu/release/stripped_ta # 4. Sign python3 /opt/optee/export-ta_arm64/scripts/sign_encrypt.py \ --uuid <uuid-from-file> \ --key /opt/optee/export-ta_arm64/keys/default_ta.pem \ --in target/aarch64-unknown-linux-gnu/release/stripped_ta \ --out target/aarch64-unknown-linux-gnu/release/<uuid>.ta
User Input:
cargo-optee build ta \ --ta-dev-kit-dir /opt/optee/export-ta_arm32 \ --manifest-path ./ta/Cargo.toml \ --arch arm \ --std
cargo-optee translates to:
# 1. Clippy cd ./ta TA_DEV_KIT_DIR=/opt/optee/export-ta_arm32 \ RUSTFLAGS="-C panic=abort" \ RUST_TARGET_PATH=/tmp/cargo-optee-XXXXX \ __CARGO_TESTS_ONLY_SRC_ROOT=/path/to/rust/library \ cargo -Z build-std=std,panic_abort -Z json-target-spec clippy --target arm-unknown-optee --features std --release \ --manifest-path ./ta/Cargo.toml # 2. Build TA_DEV_KIT_DIR=/opt/optee/export-ta_arm32 \ RUSTFLAGS="-C panic=abort" \ RUST_TARGET_PATH=/tmp/cargo-optee-XXXXX \ __CARGO_TESTS_ONLY_SRC_ROOT=/path/to/rust/library \ cargo -Z build-std=std,panic_abort -Z json-target-spec build --target arm-unknown-optee --features std --release \ --manifest-path ./ta/Cargo.toml \ --config target.arm-unknown-optee.linker="arm-linux-gnueabihf-gcc" # 3. Strip arm-linux-gnueabihf-objcopy --strip-unneeded \ target/arm-unknown-optee/release/ta \ target/arm-unknown-optee/release/stripped_ta # 4. Sign python3 /opt/optee/export-ta_arm32/scripts/sign_encrypt.py \ --uuid <uuid-from-file> \ --key /opt/optee/export-ta_arm32/keys/default_ta.pem \ --in target/arm-unknown-optee/release/stripped_ta \ --out target/arm-unknown-optee/release/<uuid>.ta
Note: /tmp/cargo-optee-XXXXX is a temporary directory containing the embedded arm-unknown-optee.json target specification.
User Input:
cargo-optee build ca \ --optee-client-export /opt/optee/export-client \ --manifest-path ./host/Cargo.toml
cargo-optee translates to:
# 1. Clippy cd ./host OPTEE_CLIENT_EXPORT=/opt/optee/export-client \ cargo clippy --target aarch64-unknown-linux-gnu \ --manifest-path ./host/Cargo.toml # 2. Build OPTEE_CLIENT_EXPORT=/opt/optee/export-client \ cargo build --target aarch64-unknown-linux-gnu --release \ --manifest-path ./host/Cargo.toml \ --config target.aarch64-unknown-linux-gnu.linker="aarch64-linux-gnu-gcc" # 3. Strip aarch64-linux-gnu-objcopy --strip-unneeded \ target/aarch64-unknown-linux-gnu/release/<binary> \ target/aarch64-unknown-linux-gnu/release/<binary>
This section explains how cargo orchestrates low-level tools to build the TA ELF binary. We use an aarch64 no-std TA as an example.
Dependency Structure:
ta
├── depends on: optee_utee (Rust API for OP-TEE TAs)
│ └── depends on: optee_utee_sys (FFI bindings to OP-TEE C API)
│ └── build.rs: outputs cargo:rustc-link-* directives
│ Links with C libraries from TA_DEV_KIT_DIR/lib/:
│ - libutee.a (OP-TEE user-space TA API)
│ - libutils.a (utility functions)
│ - libmbedtls.a (crypto library)
└── build.rs: uses optee_utee_build crate to:
- Configure TA properties (UUID, stack size, etc.)
- Generate TA header file (user_ta_header.rs)
- Output link directives
Build Flow:
Step 1: cargo-optee invokes cargo
(As shown in the previous section)
TA_DEV_KIT_DIR=/opt/optee/export-ta_arm64 \ RUSTFLAGS="-C panic=abort" \ cargo build --target aarch64-unknown-linux-gnu --release \ --config target.aarch64-unknown-linux-gnu.linker="aarch64-linux-gnu-gcc"
Step 2: cargo prepares environment and invokes build scripts
Cargo automatically sets these environment variables:
TARGET=aarch64-unknown-linux-gnuPROFILE=releaseOUT_DIR=target/aarch64-unknown-linux-gnu/release/build/ta-<hash>/outRUSTC_LINKER=aarch64-linux-gnu-gcc (from --config flag)Cargo inherits from cargo-optee:
TA_DEV_KIT_DIR=/opt/optee/export-ta_arm64RUSTFLAGS="-C panic=abort"Cargo then executes build scripts in dependency order to set up the build directives:
optee_utee_sys/build.rs
TA_DEV_KIT_DIRcargo:rustc-link-* directives to link C libraries:cargo:rustc-link-search={TA_DEV_KIT_DIR}/lib
cargo:rustc-link-lib=static=utee
cargo:rustc-link-lib=static=utils
cargo:rustc-link-lib=static=mbedtls
ta/build.rs → calls optee_utee_build crate
TA_DEV_KIT_DIR, TARGET, OUT_DIR, RUSTC_LINKERCARGO_PKG_VERSION, CARGO_PKG_DESCRIPTION (for automatic TA config)user_ta_header.rs) with TA propertiesStep 3: rustc compiles Rust source code
Rustc receives:
--target aarch64-unknown-linux-gnu-C panic=abort (from RUSTFLAGS)releaseProduces: .rlib files and object files (.o)
Step 4: gcc linker links final binary
The linker (specified by RUSTC_LINKER=aarch64-linux-gnu-gcc) links:
.o)libutee.a, libutils.a, libmbedtls.ata.ldsOutput: ELF binary at target/aarch64-unknown-linux-gnu/release/ta