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}