Skip to main content

key_vault/tee/
mod.rs

1//! Trusted Execution Environment detection.
2//!
3//! [`detect_tee_capabilities`] inspects the host platform and returns a
4//! [`TeeCapabilities`] snapshot describing which trusted execution
5//! environments are *available* — not whether the current process is *running
6//! inside* one. The vault uses this at startup to choose between
7//! `KeyFetch` implementations and to surface availability through audit
8//! records.
9//!
10//! # What 1.0 promises
11//!
12//! Detection only. Integration with the underlying enclave APIs (signing
13//! attestation reports, sealing data, running code inside SGX/TDX/SEV/SE/Nitro)
14//! is explicitly deferred to the 1.x line — see `.dev/ROADMAP.md`.
15//!
16//! # Verification semantics
17//!
18//! Each capability is reported as one of three values:
19//!
20//! - [`Detection::Detected`] — the capability is present on this host and the
21//!   detection path completed successfully.
22//! - [`Detection::NotDetected`] — the detection path completed successfully
23//!   and found no support.
24//! - [`Detection::Unknown`] — the detection path is not implemented on this
25//!   platform, or the necessary detection signal is not accessible from
26//!   userspace.
27//!
28//! Treating `Unknown` as "not available" is the safe default for selecting
29//! fetchers.
30
31use core::fmt;
32
33#[cfg(target_arch = "x86_64")]
34mod x86_64;
35
36/// Result of a single TEE capability probe.
37///
38/// `Unknown` is distinct from `NotDetected` on purpose. On platforms where we
39/// cannot run the probe (for example asking about Intel SGX from an aarch64
40/// host) we report `Unknown` rather than claim absence — callers that care
41/// about the distinction can degrade gracefully.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43#[non_exhaustive]
44pub enum Detection {
45    /// The capability is present and a fetcher backed by it would succeed.
46    Detected,
47    /// The probe ran and found no support.
48    NotDetected,
49    /// The probe is not implemented on this platform, or its signal is
50    /// inaccessible from userspace.
51    Unknown,
52}
53
54impl Detection {
55    /// `true` only if this probe positively confirmed the capability.
56    #[must_use]
57    pub fn is_detected(self) -> bool {
58        matches!(self, Self::Detected)
59    }
60}
61
62impl fmt::Display for Detection {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        let s = match self {
65            Self::Detected => "detected",
66            Self::NotDetected => "not detected",
67            Self::Unknown => "unknown",
68        };
69        f.write_str(s)
70    }
71}
72
73/// Snapshot of every TEE probe the vault knows how to run on this host.
74///
75/// Adding a new probe is a minor-version change — the struct is
76/// `#[non_exhaustive]`. Existing fields will not change meaning across the 1.x
77/// line.
78///
79/// # Examples
80///
81/// ```
82/// use key_vault::tee::{detect_tee_capabilities, Detection};
83///
84/// let caps = detect_tee_capabilities();
85/// // We cannot assert specific values — the result depends on hardware. But
86/// // every field is queryable:
87/// let _ = caps.sgx;
88/// let _ = caps.tdx;
89/// let _ = caps.sev;
90/// let _ = caps.sev_snp;
91/// let _ = caps.trustzone;
92/// let _ = caps.secure_enclave;
93/// let _ = caps.nitro;
94///
95/// // Display is implemented for human-readable summaries:
96/// let _ = format!("{caps}");
97/// ```
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99#[non_exhaustive]
100pub struct TeeCapabilities {
101    /// Intel Software Guard Extensions (SGX). Detected by CPUID leaf 7,
102    /// EBX bit 2 on x86_64. Always `Unknown` on non-x86_64.
103    pub sgx: Detection,
104
105    /// Intel Trust Domain Extensions (TDX). Detected by CPUID leaf 0x21
106    /// returning the "IntelTDX    " signature in EBX/ECX/EDX on x86_64.
107    /// Always `Unknown` on non-x86_64.
108    pub tdx: Detection,
109
110    /// AMD Secure Encrypted Virtualization (SEV). Detected by CPUID extended
111    /// leaf 0x8000001F EAX bit 1 on x86_64. Always `Unknown` on non-x86_64
112    /// or on Intel hosts.
113    pub sev: Detection,
114
115    /// AMD Secure Encrypted Virtualization — Secure Nested Paging (SEV-SNP).
116    /// Detected by CPUID extended leaf 0x8000001F EAX bit 4 on x86_64.
117    /// Always `Unknown` on non-x86_64 or on Intel hosts.
118    pub sev_snp: Detection,
119
120    /// ARM TrustZone. Userspace cannot reliably probe TrustZone availability
121    /// without privileged registers, so this is always `Unknown` in 1.0.
122    /// Operators that know their hardware supports TrustZone should configure
123    /// the vault explicitly.
124    pub trustzone: Detection,
125
126    /// Apple Secure Enclave. Reported as `Detected` on Apple Silicon
127    /// (`aarch64-apple-darwin`), `NotDetected` on Intel macOS, and `Unknown`
128    /// on non-Apple platforms.
129    pub secure_enclave: Detection,
130
131    /// AWS Nitro Enclaves availability. On Linux this is inferred from the
132    /// DMI system vendor (`/sys/devices/virtual/dmi/id/sys_vendor`); other
133    /// hosts report `Unknown`.
134    pub nitro: Detection,
135}
136
137impl TeeCapabilities {
138    /// Returns `true` if at least one probe positively confirmed a TEE.
139    ///
140    /// This is the convenience predicate for "should I prefer a hardware-backed
141    /// fetcher?". `Unknown` does not count.
142    #[must_use]
143    pub fn any_detected(self) -> bool {
144        self.sgx.is_detected()
145            || self.tdx.is_detected()
146            || self.sev.is_detected()
147            || self.sev_snp.is_detected()
148            || self.trustzone.is_detected()
149            || self.secure_enclave.is_detected()
150            || self.nitro.is_detected()
151    }
152}
153
154impl fmt::Display for TeeCapabilities {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        write!(
157            f,
158            "TeeCapabilities {{ sgx: {}, tdx: {}, sev: {}, sev_snp: {}, trustzone: {}, secure_enclave: {}, nitro: {} }}",
159            self.sgx,
160            self.tdx,
161            self.sev,
162            self.sev_snp,
163            self.trustzone,
164            self.secure_enclave,
165            self.nitro,
166        )
167    }
168}
169
170/// Run every supported TEE probe on this host and return a snapshot.
171///
172/// This is a synchronous, side-effect-free function suitable for calling at
173/// process startup. It performs a handful of CPUID instructions on x86_64 and,
174/// on Linux, reads `/sys/devices/virtual/dmi/id/sys_vendor` to detect AWS
175/// Nitro. It does **not** open privileged files, talk to the network, or
176/// load any drivers.
177///
178/// Callers can cache the result; capabilities do not change at runtime.
179#[must_use]
180pub fn detect_tee_capabilities() -> TeeCapabilities {
181    let (sgx, tdx, sev, sev_snp) = detect_x86_64();
182    TeeCapabilities {
183        sgx,
184        tdx,
185        sev,
186        sev_snp,
187        trustzone: detect_trustzone(),
188        secure_enclave: detect_secure_enclave(),
189        nitro: detect_nitro(),
190    }
191}
192
193#[cfg(target_arch = "x86_64")]
194fn detect_x86_64() -> (Detection, Detection, Detection, Detection) {
195    self::x86_64::detect()
196}
197
198#[cfg(not(target_arch = "x86_64"))]
199fn detect_x86_64() -> (Detection, Detection, Detection, Detection) {
200    (
201        Detection::Unknown,
202        Detection::Unknown,
203        Detection::Unknown,
204        Detection::Unknown,
205    )
206}
207
208#[cfg(target_arch = "aarch64")]
209fn detect_trustzone() -> Detection {
210    // Userspace cannot positively probe TrustZone without reading EL3-protected
211    // registers. Returning Unknown lets operators configure the vault
212    // explicitly without us silently misreporting.
213    Detection::Unknown
214}
215
216#[cfg(not(target_arch = "aarch64"))]
217fn detect_trustzone() -> Detection {
218    Detection::Unknown
219}
220
221#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
222fn detect_secure_enclave() -> Detection {
223    // Apple Silicon Macs (M1 and later) ship with the Secure Enclave
224    // coprocessor. Apple does not document a userspace probe; presence is
225    // implied by the CPU family.
226    Detection::Detected
227}
228
229#[cfg(all(target_os = "macos", not(target_arch = "aarch64")))]
230fn detect_secure_enclave() -> Detection {
231    // Intel Macs with a T2 chip also have a Secure Enclave, but we have no
232    // portable userspace probe for the T2. Report NotDetected on Intel macOS
233    // — callers that know they are on a T2 host should configure manually.
234    Detection::NotDetected
235}
236
237#[cfg(not(target_os = "macos"))]
238fn detect_secure_enclave() -> Detection {
239    Detection::Unknown
240}
241
242#[cfg(target_os = "linux")]
243fn detect_nitro() -> Detection {
244    // AWS sets `sys_vendor` to "Amazon EC2" on Nitro-backed instances. This
245    // is a heuristic — it distinguishes Nitro instances from non-Nitro EC2 —
246    // and it does not by itself prove that nitro-enclaves is configured.
247    // For 1.0 detection-only semantics this is the right granularity.
248    match std::fs::read_to_string("/sys/devices/virtual/dmi/id/sys_vendor") {
249        Ok(vendor) => {
250            if vendor.trim().eq_ignore_ascii_case("Amazon EC2") {
251                Detection::Detected
252            } else {
253                Detection::NotDetected
254            }
255        }
256        Err(_) => Detection::Unknown,
257    }
258}
259
260#[cfg(not(target_os = "linux"))]
261fn detect_nitro() -> Detection {
262    Detection::Unknown
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use alloc::format;
269
270    #[test]
271    fn detection_renders_human_readable() {
272        assert_eq!(format!("{}", Detection::Detected), "detected");
273        assert_eq!(format!("{}", Detection::NotDetected), "not detected");
274        assert_eq!(format!("{}", Detection::Unknown), "unknown");
275    }
276
277    #[test]
278    fn detection_is_detected_predicate() {
279        assert!(Detection::Detected.is_detected());
280        assert!(!Detection::NotDetected.is_detected());
281        assert!(!Detection::Unknown.is_detected());
282    }
283
284    #[test]
285    fn detect_tee_capabilities_does_not_panic() {
286        // We can't assert specific values — this runs on heterogeneous CI
287        // hosts. But the function must complete cleanly on every supported
288        // target.
289        let caps = detect_tee_capabilities();
290        let _ = format!("{caps}");
291        let _ = caps.any_detected();
292    }
293
294    #[cfg(not(target_arch = "x86_64"))]
295    #[test]
296    fn non_x86_64_reports_unknown_for_intel_amd() {
297        let caps = detect_tee_capabilities();
298        assert_eq!(caps.sgx, Detection::Unknown);
299        assert_eq!(caps.tdx, Detection::Unknown);
300        assert_eq!(caps.sev, Detection::Unknown);
301        assert_eq!(caps.sev_snp, Detection::Unknown);
302    }
303}