Skip to main content

consortium_builder/compile/
optee.rs

1// Copyright 2026 Ethan Wu
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! OP-TEE Trusted Application compilation and signing.
18//!
19//! A TA is not built like the rest of the system. It targets the OP-TEE TA dev kit
20//! rather than a bare-metal or Linux target, and the deployable artifact is a
21//! **signed** `<uuid>.ta` rather than the ELF the compiler produced — the UUID comes
22//! from `[tee]` in `Consortium.toml`, injected as `CONSORTIUM_TEE_UUID`.
23//!
24//! Signing needs the dev kit's signer script and its keys, which a developer host often
25//! does not have. So the pipeline supports building without signing: the unsigned ELF is
26//! staged instead, and the CLI exposes that as `--no-sign-ta`. That keeps the TA
27//! compiling in CI and on a laptop, with signing deferred to wherever the keys live.
28//!
29//! [`OpteeEnvironment`] carries the dev-kit paths the build needs.
30
31use consortium_cfg_common::config::TEE_UUID_ENV;
32use std::path::PathBuf;
33use thiserror::Error;
34use tokio::process::Command;
35use uuid::Uuid;
36
37use super::Compiler;
38use crate::compile::env::CstiWorkspace;
39use crate::compile::utils::{
40    binary_name, elf_path, inject_csti, resolve_cargo, run_command, verify_elf,
41};
42use crate::progress::BuildReporter;
43
44// Only aarch64-unknown-linux-gnu is currently supported for OP-TEE builds.
45// Armv7 targets (Cortex-A7) are not supported by the OP-TEE SDK, and
46// Cortex-M targets are out of scope for now.
47pub const OPTEE_TARGET: &str = "aarch64-unknown-linux-gnu";
48
49/// Error type for OP-TEE compilation and signing.
50#[derive(Error, Debug)]
51pub enum OpteeCompileError {
52    #[error("TA_DEV_KIT_DIR does not exist: {0:?}")]
53    TaDevKitNotFound(PathBuf),
54
55    #[error("OP-TEE signer not found at: {0:?}")]
56    SignerNotFound(PathBuf),
57
58    #[error("TA signing key not found at: {0:?}")]
59    SigningKeyNotFound(PathBuf),
60
61    #[error("Python interpreter not found on PATH (tried: python, python3)")]
62    PythonNotFound,
63
64    #[error("Cargo failed: {0}")]
65    BuildFailed(String),
66
67    #[error("ELF not found at expected path: {0:?}")]
68    ElfNotFound(PathBuf),
69
70    #[error("OP-TEE signing failed: {0}")]
71    SigningFailed(String),
72
73    #[error("Signed TA not found after signing: {0:?}")]
74    SignedTaNotFound(PathBuf),
75
76    #[error("{0}")]
77    UtilityError(String),
78}
79
80/// All build + signing parameters for an OP-TEE Trusted Application.
81pub struct OpteeConfiguration {
82    /// Path to the Cargo project root.
83    pub path: PathBuf,
84    /// Build in release mode.
85    pub release: bool,
86    /// Pass `--offline` to cargo: resolve and build from the local registry cache
87    /// only. Populate that cache first with `csti fetch`.
88    pub offline: bool,
89    /// Optional explicit linker (e.g. the cross-compiler from the OP-TEE SDK).
90    pub linker: Option<String>,
91    /// Path to the OP-TEE TA dev-kit directory (`TA_DEV_KIT_DIR`).
92    pub ta_dev_kit_dir: PathBuf,
93    /// Path to the OP-TEE signer script (e.g. `sign_encrypt.py`).
94    pub optee_signer: PathBuf,
95    /// UUID assigned to this Trusted Application.
96    pub ta_uuid: Uuid,
97    /// Sign the built ELF into a loadable `<uuid>.ta`. When `false`, the build
98    /// stops after producing (and verifying) the unsigned ELF and returns that
99    /// path — useful when the OP-TEE signer/keys are unavailable and you only
100    /// need to check that the TA compiles and links.
101    pub sign: bool,
102    /// Build-host directories injected as `CSTI_*` env vars, and the workspace
103    /// layout locating `target/`. `None` injects nothing and uses the crate's
104    /// own `target/`.
105    pub workspace: Option<CstiWorkspace>,
106}
107
108/// Compiler for OP-TEE Trusted Applications: builds the ELF then signs it.
109pub struct OpteeCompiler;
110
111impl OpteeCompiler {
112    pub async fn build_with_reporter(
113        config: OpteeConfiguration,
114        reporter: Option<&dyn BuildReporter>,
115    ) -> Result<PathBuf, OpteeCompileError> {
116        // ── Step 1: build ───────────────────────────────────────────────────
117        Self::cargo(&config, "build", reporter).await?;
118
119        let elf = Self::output_path(&config).await?;
120
121        if !elf.exists() {
122            return Err(OpteeCompileError::ElfNotFound(elf));
123        }
124
125        verify_elf(&elf)
126            .await
127            .map_err(OpteeCompileError::UtilityError)?;
128
129        // Stop at the unsigned ELF when signing is disabled (no signer/keys
130        // required). The dist stage stages whatever path is returned here.
131        if !config.sign {
132            return Ok(elf);
133        }
134
135        // ── Step 2: sign ────────────────────────────────────────────────────
136        if !config.optee_signer.exists() {
137            return Err(OpteeCompileError::SignerNotFound(config.optee_signer));
138        }
139
140        let ta_key = config.ta_dev_kit_dir.join("keys").join("default_ta.pem");
141        if !ta_key.exists() {
142            return Err(OpteeCompileError::SigningKeyNotFound(ta_key));
143        }
144
145        let python = resolve_python().await?;
146
147        let ta = elf
148            .parent()
149            .ok_or_else(|| {
150                OpteeCompileError::UtilityError(format!(
151                    "Could not determine output directory for ELF: {elf:?}"
152                ))
153            })?
154            .join(format!("{}.ta", config.ta_uuid));
155
156        let mut sign_cmd = Command::new(&python);
157        sign_cmd
158            .arg(&config.optee_signer)
159            .arg("--uuid")
160            .arg(config.ta_uuid.to_string())
161            .arg("--key")
162            .arg(&ta_key)
163            .arg("--in")
164            .arg(&elf)
165            .arg("--out")
166            .arg(&ta)
167            .env("TA_DEV_KIT_DIR", &config.ta_dev_kit_dir);
168
169        let status = run_command(&mut sign_cmd, reporter).await.map_err(|e| {
170            OpteeCompileError::SigningFailed(format!(
171                "Failed to execute OP-TEE signer via {python}: {e}"
172            ))
173        })?;
174
175        if !status.success() {
176            return Err(OpteeCompileError::SigningFailed(format!(
177                "OP-TEE signing failed with status: {status}"
178            )));
179        }
180
181        if !ta.exists() {
182            return Err(OpteeCompileError::SignedTaNotFound(ta));
183        }
184
185        Ok(ta)
186    }
187
188    /// Type-checks the TA binary with `cargo check`, under the same dev kit,
189    /// features, and flags as [`build_with_reporter`](Self::build_with_reporter).
190    /// Produces no ELF and never signs, so no signer or keys are needed.
191    pub async fn check_with_reporter(
192        config: &OpteeConfiguration,
193        reporter: Option<&dyn BuildReporter>,
194    ) -> Result<(), OpteeCompileError> {
195        Self::cargo(config, "check", reporter).await
196    }
197
198    /// Runs `cargo <subcommand>` for the TA's feature-gated binary.
199    async fn cargo(
200        config: &OpteeConfiguration,
201        subcommand: &str,
202        reporter: Option<&dyn BuildReporter>,
203    ) -> Result<(), OpteeCompileError> {
204        if !config.ta_dev_kit_dir.exists() {
205            return Err(OpteeCompileError::TaDevKitNotFound(
206                config.ta_dev_kit_dir.clone(),
207            ));
208        }
209
210        let cargo = resolve_cargo()
211            .await
212            .map_err(OpteeCompileError::UtilityError)?;
213
214        // The TA ELF is the `[[bin]]` target (the `#[ta_*]` entry points + the
215        // generated `user_ta_header.rs`), gated behind the `ta` feature. Build it
216        // explicitly: a plain `cargo build`/`check` would compile only the default-feature
217        // `rlib` (the `ca` callers) and skip the feature-gated binary entirely.
218        let bin = binary_name(&config.path)
219            .await
220            .map_err(OpteeCompileError::UtilityError)?;
221
222        let mut cmd = Command::new(&cargo);
223        cmd.arg(subcommand)
224            .arg("--bin")
225            .arg(&bin)
226            .arg("--features")
227            .arg("ta")
228            .arg("--target")
229            .arg(OPTEE_TARGET)
230            .current_dir(&config.path)
231            .env("TA_DEV_KIT_DIR", &config.ta_dev_kit_dir)
232            .env(TEE_UUID_ENV, config.ta_uuid.to_string())
233            .env("RUSTFLAGS", "-C panic=abort");
234        inject_csti(&mut cmd, config.workspace.as_ref());
235
236        if config.release {
237            cmd.arg("--release");
238        }
239
240        if config.offline {
241            cmd.arg("--offline");
242        }
243
244        if let Some(ref linker) = config.linker {
245            cmd.arg("--config")
246                .arg(format!("{OPTEE_TARGET}.linker={linker}"));
247        }
248
249        let status = run_command(&mut cmd, reporter)
250            .await
251            .map_err(|e| OpteeCompileError::BuildFailed(e.to_string()))?;
252
253        if !status.success() {
254            return Err(OpteeCompileError::BuildFailed(format!(
255                "cargo {subcommand} failed with status: {status}"
256            )));
257        }
258        Ok(())
259    }
260}
261
262impl Compiler for OpteeCompiler {
263    type Configuration = OpteeConfiguration;
264    type Error = OpteeCompileError;
265    type Output = PathBuf;
266
267    async fn build(config: Self::Configuration) -> Result<Self::Output, Self::Error> {
268        Self::build_with_reporter(config, None).await
269    }
270
271    async fn output_path(config: &Self::Configuration) -> Result<PathBuf, Self::Error> {
272        elf_path(
273            &config.path,
274            config.workspace.as_ref(),
275            OPTEE_TARGET,
276            config.release,
277        )
278        .await
279        .map_err(OpteeCompileError::UtilityError)
280    }
281}
282
283/// Backward-compatible builder; delegates to [`OpteeCompiler`].
284///
285/// Always builds online (`offline: false`); use [`OpteeCompiler`] directly to
286/// build from the local registry cache.
287pub struct OpteeBuilder {
288    /// Path to the Cargo project root.
289    pub path: PathBuf,
290    pub release: bool,
291    pub linker: Option<String>,
292}
293
294/// Backward-compatible environment for [`OpteeBuilder`].
295pub struct OpteeEnvironment {
296    pub ta_dev_kit_dir: PathBuf,
297    pub optee_signer: PathBuf,
298    pub ta_uuid: Uuid,
299}
300
301impl OpteeBuilder {
302    pub async fn build(&self, env: &OpteeEnvironment) -> Result<PathBuf, String> {
303        OpteeCompiler::build(OpteeConfiguration {
304            path: self.path.clone(),
305            release: self.release,
306            offline: false,
307            linker: self.linker.clone(),
308            ta_dev_kit_dir: env.ta_dev_kit_dir.clone(),
309            optee_signer: env.optee_signer.clone(),
310            ta_uuid: env.ta_uuid,
311            sign: true,
312            workspace: None,
313        })
314        .await
315        .map_err(|e| e.to_string())
316    }
317}
318
319async fn resolve_python() -> Result<String, OpteeCompileError> {
320    for candidate in ["python", "python3"] {
321        if let Ok(status) = Command::new(candidate).arg("--version").status().await
322            && status.success()
323        {
324            return Ok(candidate.to_string());
325        }
326    }
327    Err(OpteeCompileError::PythonNotFound)
328}