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