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::utils::{binary_name, elf_path, resolve_cargo, run_command, verify_elf};
39use crate::progress::BuildReporter;
40
41// Only aarch64-unknown-linux-gnu is currently supported for OP-TEE builds.
42// Armv7 targets (Cortex-A7) are not supported by the OP-TEE SDK, and
43// Cortex-M targets are out of scope for now.
44pub const OPTEE_TARGET: &str = "aarch64-unknown-linux-gnu";
45
46/// Error type for OP-TEE compilation and signing.
47#[derive(Error, Debug)]
48pub enum OpteeCompileError {
49    #[error("TA_DEV_KIT_DIR does not exist: {0:?}")]
50    TaDevKitNotFound(PathBuf),
51
52    #[error("OP-TEE signer not found at: {0:?}")]
53    SignerNotFound(PathBuf),
54
55    #[error("TA signing key not found at: {0:?}")]
56    SigningKeyNotFound(PathBuf),
57
58    #[error("Python interpreter not found on PATH (tried: python, python3)")]
59    PythonNotFound,
60
61    #[error("Cargo build failed: {0}")]
62    BuildFailed(String),
63
64    #[error("ELF not found at expected path: {0:?}")]
65    ElfNotFound(PathBuf),
66
67    #[error("OP-TEE signing failed: {0}")]
68    SigningFailed(String),
69
70    #[error("Signed TA not found after signing: {0:?}")]
71    SignedTaNotFound(PathBuf),
72
73    #[error("{0}")]
74    UtilityError(String),
75}
76
77/// All build + signing parameters for an OP-TEE Trusted Application.
78pub struct OpteeConfiguration {
79    /// Path to the Cargo project root.
80    pub path: PathBuf,
81    /// Build in release mode.
82    pub release: bool,
83    /// Optional explicit linker (e.g. the cross-compiler from the OP-TEE SDK).
84    pub linker: Option<String>,
85    /// Path to the OP-TEE TA dev-kit directory (`TA_DEV_KIT_DIR`).
86    pub ta_dev_kit_dir: PathBuf,
87    /// Path to the OP-TEE signer script (e.g. `sign_encrypt.py`).
88    pub optee_signer: PathBuf,
89    /// UUID assigned to this Trusted Application.
90    pub ta_uuid: Uuid,
91    /// Sign the built ELF into a loadable `<uuid>.ta`. When `false`, the build
92    /// stops after producing (and verifying) the unsigned ELF and returns that
93    /// path — useful when the OP-TEE signer/keys are unavailable and you only
94    /// need to check that the TA compiles and links.
95    pub sign: bool,
96}
97
98/// Compiler for OP-TEE Trusted Applications: builds the ELF then signs it.
99pub struct OpteeCompiler;
100
101impl OpteeCompiler {
102    pub async fn build_with_reporter(
103        config: OpteeConfiguration,
104        reporter: Option<&dyn BuildReporter>,
105    ) -> Result<PathBuf, OpteeCompileError> {
106        // ── Step 1: build ───────────────────────────────────────────────────
107        if !config.ta_dev_kit_dir.exists() {
108            return Err(OpteeCompileError::TaDevKitNotFound(config.ta_dev_kit_dir));
109        }
110
111        let cargo = resolve_cargo()
112            .await
113            .map_err(OpteeCompileError::UtilityError)?;
114
115        // The TA ELF is the `[[bin]]` target (the `#[ta_*]` entry points + the
116        // generated `user_ta_header.rs`), gated behind the `ta` feature. Build it
117        // explicitly: a plain `cargo build` would compile only the default-feature
118        // `rlib` (the `ca` callers) and skip the feature-gated binary entirely.
119        let bin = binary_name(&config.path)
120            .await
121            .map_err(OpteeCompileError::UtilityError)?;
122
123        let mut cmd = Command::new(&cargo);
124        cmd.arg("build")
125            .arg("--bin")
126            .arg(&bin)
127            .arg("--features")
128            .arg("ta")
129            .arg("--target")
130            .arg(OPTEE_TARGET)
131            .current_dir(&config.path)
132            .env("TA_DEV_KIT_DIR", &config.ta_dev_kit_dir)
133            .env(TEE_UUID_ENV, config.ta_uuid.to_string())
134            .env("RUSTFLAGS", "-C panic=abort");
135
136        if config.release {
137            cmd.arg("--release");
138        }
139
140        if let Some(ref linker) = config.linker {
141            cmd.arg("--config")
142                .arg(format!("{OPTEE_TARGET}.linker={linker}"));
143        }
144
145        let status = run_command(&mut cmd, reporter)
146            .await
147            .map_err(|e| OpteeCompileError::BuildFailed(e.to_string()))?;
148
149        if !status.success() {
150            return Err(OpteeCompileError::BuildFailed(format!(
151                "cargo build failed with status: {status}"
152            )));
153        }
154
155        let elf = Self::output_path(&config).await?;
156
157        if !elf.exists() {
158            return Err(OpteeCompileError::ElfNotFound(elf));
159        }
160
161        verify_elf(&elf)
162            .await
163            .map_err(OpteeCompileError::UtilityError)?;
164
165        // Stop at the unsigned ELF when signing is disabled (no signer/keys
166        // required). The dist stage stages whatever path is returned here.
167        if !config.sign {
168            return Ok(elf);
169        }
170
171        // ── Step 2: sign ────────────────────────────────────────────────────
172        if !config.optee_signer.exists() {
173            return Err(OpteeCompileError::SignerNotFound(config.optee_signer));
174        }
175
176        let ta_key = config.ta_dev_kit_dir.join("keys").join("default_ta.pem");
177        if !ta_key.exists() {
178            return Err(OpteeCompileError::SigningKeyNotFound(ta_key));
179        }
180
181        let python = resolve_python().await?;
182
183        let ta = elf
184            .parent()
185            .ok_or_else(|| {
186                OpteeCompileError::UtilityError(format!(
187                    "Could not determine output directory for ELF: {elf:?}"
188                ))
189            })?
190            .join(format!("{}.ta", config.ta_uuid));
191
192        let mut sign_cmd = Command::new(&python);
193        sign_cmd
194            .arg(&config.optee_signer)
195            .arg("--uuid")
196            .arg(config.ta_uuid.to_string())
197            .arg("--key")
198            .arg(&ta_key)
199            .arg("--in")
200            .arg(&elf)
201            .arg("--out")
202            .arg(&ta)
203            .env("TA_DEV_KIT_DIR", &config.ta_dev_kit_dir);
204
205        let status = run_command(&mut sign_cmd, reporter).await.map_err(|e| {
206            OpteeCompileError::SigningFailed(format!(
207                "Failed to execute OP-TEE signer via {python}: {e}"
208            ))
209        })?;
210
211        if !status.success() {
212            return Err(OpteeCompileError::SigningFailed(format!(
213                "OP-TEE signing failed with status: {status}"
214            )));
215        }
216
217        if !ta.exists() {
218            return Err(OpteeCompileError::SignedTaNotFound(ta));
219        }
220
221        Ok(ta)
222    }
223}
224
225impl Compiler for OpteeCompiler {
226    type Configuration = OpteeConfiguration;
227    type Error = OpteeCompileError;
228    type Output = PathBuf;
229
230    async fn build(config: Self::Configuration) -> Result<Self::Output, Self::Error> {
231        Self::build_with_reporter(config, None).await
232    }
233
234    async fn output_path(config: &Self::Configuration) -> Result<PathBuf, Self::Error> {
235        elf_path(&config.path, OPTEE_TARGET, config.release)
236            .await
237            .map_err(OpteeCompileError::UtilityError)
238    }
239}
240
241/// Backward-compatible builder; delegates to [`OpteeCompiler`].
242pub struct OpteeBuilder {
243    /// Path to the Cargo project root.
244    pub path: PathBuf,
245    pub release: bool,
246    pub linker: Option<String>,
247}
248
249/// Backward-compatible environment for [`OpteeBuilder`].
250pub struct OpteeEnvironment {
251    pub ta_dev_kit_dir: PathBuf,
252    pub optee_signer: PathBuf,
253    pub ta_uuid: Uuid,
254}
255
256impl OpteeBuilder {
257    pub async fn build(&self, env: &OpteeEnvironment) -> Result<PathBuf, String> {
258        OpteeCompiler::build(OpteeConfiguration {
259            path: self.path.clone(),
260            release: self.release,
261            linker: self.linker.clone(),
262            ta_dev_kit_dir: env.ta_dev_kit_dir.clone(),
263            optee_signer: env.optee_signer.clone(),
264            ta_uuid: env.ta_uuid,
265            sign: true,
266        })
267        .await
268        .map_err(|e| e.to_string())
269    }
270}
271
272async fn resolve_python() -> Result<String, OpteeCompileError> {
273    for candidate in ["python", "python3"] {
274        if let Ok(status) = Command::new(candidate).arg("--version").status().await
275            && status.success()
276        {
277            return Ok(candidate.to_string());
278        }
279    }
280    Err(OpteeCompileError::PythonNotFound)
281}