Skip to main content

hardware_enclave/internal/tpm_bridge/
tpm.rs

1// Copyright 2026 Jay Gowdy
2// SPDX-License-Identifier: MIT
3
4//! enclave.
5//!
6//! On Windows, this uses `enclaveapp-windows::TpmEncryptor` to perform
7//! hardware-backed ECIES encryption via the Windows CNG/NCrypt APIs.
8//!
9//! On non-Windows platforms, all operations return an error at runtime.
10#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
11
12use crate::internal::core::metadata;
13use crate::internal::core::traits::{EnclaveEncryptor, EnclaveKeyManager, EnclaveSigner};
14use crate::internal::core::types::{AccessPolicy, KeyType};
15use std::path::Path;
16
17#[cfg_attr(not(any(test, target_os = "windows")), allow(dead_code))]
18fn existing_policy(keys_dir: &Path, key_label: &str) -> Option<AccessPolicy> {
19    let meta_path = keys_dir.join(format!("{key_label}.meta"));
20    if !meta_path.exists() {
21        return None;
22    }
23    metadata::load_meta(keys_dir, key_label)
24        .ok()
25        .map(|meta| meta.access_policy)
26}
27
28#[cfg_attr(not(any(test, target_os = "windows")), allow(dead_code))]
29pub(crate) fn ensure_key<E>(
30    encryptor: &E,
31    keys_dir: &Path,
32    key_label: &str,
33    policy: AccessPolicy,
34) -> Result<(), String>
35where
36    E: EnclaveEncryptor + EnclaveKeyManager,
37{
38    if encryptor.public_key(key_label).is_ok() {
39        match existing_policy(keys_dir, key_label) {
40            Some(existing) if existing != policy => {
41                encryptor
42                    .delete_key(key_label)
43                    .map_err(|e| format!("key deletion failed: {e}"))?;
44            }
45            _ => return Ok(()),
46        }
47    }
48
49    encryptor
50        .generate(key_label, KeyType::Encryption, policy)
51        .map_err(|e| format!("key generation failed: {e}"))?;
52    Ok(())
53}
54
55#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
56pub(crate) fn ensure_signing_key<S>(
57    signer: &S,
58    keys_dir: &Path,
59    key_label: &str,
60    policy: AccessPolicy,
61) -> Result<(), String>
62where
63    S: EnclaveSigner + EnclaveKeyManager,
64{
65    if signer.public_key(key_label).is_ok() {
66        match existing_policy(keys_dir, key_label) {
67            Some(existing) if existing != policy => {
68                signer
69                    .delete_key(key_label)
70                    .map_err(|e| format!("key deletion failed: {e}"))?;
71            }
72            _ => return Ok(()),
73        }
74    }
75
76    signer
77        .generate(key_label, KeyType::Signing, policy)
78        .map_err(|e| format!("key generation failed: {e}"))?;
79    Ok(())
80}
81
82#[cfg(target_os = "windows")]
83mod platform {
84    use super::{ensure_key, ensure_signing_key, metadata};
85    use crate::internal::core::traits::{EnclaveEncryptor, EnclaveKeyManager, EnclaveSigner};
86    use crate::internal::core::types::AccessPolicy;
87    use crate::internal::windows::{TpmEncryptor, TpmSigner};
88
89    pub struct TpmStorage {
90        encryptor: TpmEncryptor,
91        key_label: String,
92    }
93
94    impl std::fmt::Debug for TpmStorage {
95        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96            f.debug_struct("TpmStorage")
97                .field("key_label", &self.key_label)
98                .finish_non_exhaustive()
99        }
100    }
101
102    impl TpmStorage {
103        pub fn new(
104            app_name: &str,
105            key_label: &str,
106            access_policy: AccessPolicy,
107        ) -> Result<Self, String> {
108            let encryptor = TpmEncryptor::new(app_name);
109
110            if !encryptor.is_available() {
111                return Err("TPM not available".to_string());
112            }
113
114            ensure_key(
115                &encryptor,
116                &metadata::keys_dir(app_name),
117                key_label,
118                access_policy,
119            )?;
120
121            Ok(Self {
122                encryptor,
123                key_label: key_label.to_string(),
124            })
125        }
126
127        pub fn delete(app_name: &str, key_label: &str) -> Result<(), String> {
128            let encryptor = TpmEncryptor::new(app_name);
129
130            if !encryptor.is_available() {
131                return Err("TPM not available".to_string());
132            }
133
134            encryptor
135                .delete_key(key_label)
136                .map_err(|e| format!("key delete failed: {e}"))
137        }
138
139        pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, String> {
140            self.encryptor
141                .encrypt(&self.key_label, plaintext)
142                .map_err(|e| e.to_string())
143        }
144
145        pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, String> {
146            self.encryptor
147                .decrypt(&self.key_label, ciphertext)
148                .map_err(|e| e.to_string())
149        }
150    }
151
152    pub struct TpmSigningStorage {
153        signer: TpmSigner,
154        key_label: String,
155    }
156
157    impl std::fmt::Debug for TpmSigningStorage {
158        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159            f.debug_struct("TpmSigningStorage")
160                .field("key_label", &self.key_label)
161                .finish_non_exhaustive()
162        }
163    }
164
165    impl TpmSigningStorage {
166        pub fn new(
167            app_name: &str,
168            key_label: &str,
169            access_policy: AccessPolicy,
170        ) -> Result<Self, String> {
171            let signer = TpmSigner::new(app_name);
172
173            if !signer.is_available() {
174                return Err("TPM not available".to_string());
175            }
176
177            ensure_signing_key(
178                &signer,
179                &metadata::keys_dir(app_name),
180                key_label,
181                access_policy,
182            )?;
183
184            Ok(Self {
185                signer,
186                key_label: key_label.to_string(),
187            })
188        }
189
190        pub fn sign(&self, data: &[u8]) -> Result<Vec<u8>, String> {
191            self.signer
192                .sign(&self.key_label, data)
193                .map_err(|e| e.to_string())
194        }
195
196        pub fn public_key(&self) -> Result<Vec<u8>, String> {
197            self.signer
198                .public_key(&self.key_label)
199                .map_err(|e| e.to_string())
200        }
201
202        pub fn list_keys(&self) -> Result<Vec<String>, String> {
203            self.signer.list_keys().map_err(|e| e.to_string())
204        }
205
206        /// List signing keys for an app without requiring a per-label
207        /// `init_signing`. Unlike the instance method above, this is
208        /// the path the bridge server uses for `list_keys` so the
209        /// agent's identity-enumeration doesn't create a `default`
210        /// key as a side effect of the per-key init_signing prelude.
211        pub fn list_keys_for_app(app_name: &str) -> Result<Vec<String>, String> {
212            let signer = TpmSigner::new(app_name);
213            if !signer.is_available() {
214                return Err("TPM not available".to_string());
215            }
216            signer.list_keys().map_err(|e| e.to_string())
217        }
218
219        /// Read the public key for an existing (app_name, key_label)
220        /// pair without `init_signing`. Same shape as the
221        /// `list_keys_for_app` standalone helper added in enclave PR #110
222        /// PR #110: lets the bridge server respond to a `public_key`
223        /// request without going through TpmSigningStorage::new (which
224        /// has create-if-missing semantics). If the key doesn't exist,
225        /// the underlying `TpmSigner::public_key` returns
226        /// `Error::KeyNotFound`, which is the right shape for clients
227        /// to handle.
228        pub fn public_key_for_app(app_name: &str, key_label: &str) -> Result<Vec<u8>, String> {
229            let signer = TpmSigner::new(app_name);
230            if !signer.is_available() {
231                return Err("TPM not available".to_string());
232            }
233            signer.public_key(key_label).map_err(|e| e.to_string())
234        }
235
236        pub fn delete(app_name: &str, key_label: &str) -> Result<(), String> {
237            let signer = TpmSigner::new(app_name);
238
239            if !signer.is_available() {
240                return Err("TPM not available".to_string());
241            }
242
243            signer
244                .delete_key(key_label)
245                .map_err(|e| format!("key delete failed: {e}"))
246        }
247
248        /// Check if a signing key exists without creating it. Unlike `new()`,
249        /// this does not call `ensure_signing_key` so it has no side effects.
250        pub fn key_exists(app_name: &str, key_label: &str) -> Result<bool, String> {
251            let signer = TpmSigner::new(app_name);
252
253            if !signer.is_available() {
254                return Err("TPM not available".to_string());
255            }
256
257            match signer.public_key(key_label) {
258                Ok(_) => Ok(true),
259                Err(crate::internal::core::Error::KeyNotFound { .. }) => Ok(false),
260                Err(e) => Err(e.to_string()),
261            }
262        }
263    }
264}
265
266#[cfg(not(target_os = "windows"))]
267mod platform {
268    use crate::internal::core::types::AccessPolicy;
269
270    #[derive(Debug)]
271    pub struct TpmStorage {
272        _app_name: String,
273        _key_label: String,
274        _access_policy: AccessPolicy,
275    }
276
277    impl TpmStorage {
278        #[allow(clippy::unnecessary_wraps)]
279        pub fn new(
280            app_name: &str,
281            key_label: &str,
282            access_policy: AccessPolicy,
283        ) -> Result<Self, String> {
284            Ok(Self {
285                _app_name: app_name.to_string(),
286                _key_label: key_label.to_string(),
287                _access_policy: access_policy,
288            })
289        }
290
291        #[allow(clippy::unnecessary_wraps)]
292        pub fn delete(_app_name: &str, _key_label: &str) -> Result<(), String> {
293            Ok(())
294        }
295
296        #[allow(clippy::unused_self)]
297        pub fn encrypt(&self, _plaintext: &[u8]) -> Result<Vec<u8>, String> {
298            Err("TPM bridge is only supported on Windows".to_string())
299        }
300
301        #[allow(clippy::unused_self)]
302        pub fn decrypt(&self, _ciphertext: &[u8]) -> Result<Vec<u8>, String> {
303            Err("TPM bridge is only supported on Windows".to_string())
304        }
305    }
306
307    #[derive(Debug)]
308    pub struct TpmSigningStorage {
309        _app_name: String,
310        _key_label: String,
311        _access_policy: AccessPolicy,
312    }
313
314    impl TpmSigningStorage {
315        #[allow(clippy::unnecessary_wraps)]
316        pub fn new(
317            app_name: &str,
318            key_label: &str,
319            access_policy: AccessPolicy,
320        ) -> Result<Self, String> {
321            Ok(Self {
322                _app_name: app_name.to_string(),
323                _key_label: key_label.to_string(),
324                _access_policy: access_policy,
325            })
326        }
327
328        #[allow(clippy::unused_self)]
329        pub fn sign(&self, _data: &[u8]) -> Result<Vec<u8>, String> {
330            Err("TPM signing bridge is only supported on Windows".to_string())
331        }
332
333        #[allow(clippy::unused_self)]
334        pub fn public_key(&self) -> Result<Vec<u8>, String> {
335            Err("TPM signing bridge is only supported on Windows".to_string())
336        }
337
338        #[allow(clippy::unused_self)]
339        pub fn list_keys(&self) -> Result<Vec<String>, String> {
340            Err("TPM signing bridge is only supported on Windows".to_string())
341        }
342
343        pub fn list_keys_for_app(_app_name: &str) -> Result<Vec<String>, String> {
344            Err("TPM signing bridge is only supported on Windows".to_string())
345        }
346
347        pub fn public_key_for_app(_app_name: &str, _key_label: &str) -> Result<Vec<u8>, String> {
348            Err("TPM signing bridge is only supported on Windows".to_string())
349        }
350
351        #[allow(clippy::unnecessary_wraps)]
352        pub fn delete(_app_name: &str, _key_label: &str) -> Result<(), String> {
353            Ok(())
354        }
355
356        pub fn key_exists(_app_name: &str, _key_label: &str) -> Result<bool, String> {
357            Err("TPM signing bridge is only supported on Windows".to_string())
358        }
359    }
360}
361
362pub use platform::TpmSigningStorage;
363pub use platform::TpmStorage;
364
365#[cfg(test)]
366#[allow(clippy::unwrap_used, clippy::panic)]
367mod tests {
368    use super::*;
369    use crate::internal::core::{Error, Result};
370    use std::sync::atomic::{AtomicU64, Ordering};
371    use std::sync::Mutex;
372
373    static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
374
375    fn test_dir() -> std::path::PathBuf {
376        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
377        let pid = std::process::id();
378        let dir = std::env::temp_dir().join(format!("enclaveapp-tpm-bridge-test-{pid}-{id}"));
379        std::fs::create_dir_all(&dir).unwrap();
380        dir
381    }
382
383    #[derive(Default)]
384    struct FakeState {
385        has_key: bool,
386        deleted: Vec<String>,
387        generated: Vec<(String, KeyType, AccessPolicy)>,
388    }
389
390    #[derive(Default)]
391    struct FakeEncryptor {
392        state: Mutex<FakeState>,
393    }
394
395    impl FakeEncryptor {
396        fn with_existing_key() -> Self {
397            Self {
398                state: Mutex::new(FakeState {
399                    has_key: true,
400                    deleted: Vec::new(),
401                    generated: Vec::new(),
402                }),
403            }
404        }
405
406        fn deleted_labels(&self) -> Vec<String> {
407            self.state.lock().unwrap().deleted.clone()
408        }
409
410        fn generated_calls(&self) -> Vec<(String, KeyType, AccessPolicy)> {
411            self.state.lock().unwrap().generated.clone()
412        }
413    }
414
415    impl EnclaveKeyManager for FakeEncryptor {
416        fn generate(
417            &self,
418            label: &str,
419            key_type: KeyType,
420            policy: AccessPolicy,
421        ) -> Result<Vec<u8>> {
422            let mut state = self.state.lock().map_err(|e| Error::KeyOperation {
423                operation: "lock".to_string(),
424                detail: e.to_string(),
425            })?;
426            state.has_key = true;
427            state.generated.push((label.to_string(), key_type, policy));
428            Ok(vec![0x04; 65])
429        }
430
431        fn public_key(&self, label: &str) -> Result<Vec<u8>> {
432            let state = self.state.lock().map_err(|e| Error::KeyOperation {
433                operation: "lock".to_string(),
434                detail: e.to_string(),
435            })?;
436            if state.has_key {
437                Ok(vec![0x04; 65])
438            } else {
439                Err(Error::KeyNotFound {
440                    label: label.to_string(),
441                })
442            }
443        }
444
445        fn list_keys(&self) -> Result<Vec<String>> {
446            Ok(Vec::new())
447        }
448
449        fn delete_key(&self, label: &str) -> Result<()> {
450            let mut state = self.state.lock().map_err(|e| Error::KeyOperation {
451                operation: "lock".to_string(),
452                detail: e.to_string(),
453            })?;
454            state.has_key = false;
455            state.deleted.push(label.to_string());
456            Ok(())
457        }
458
459        fn is_available(&self) -> bool {
460            true
461        }
462    }
463
464    impl EnclaveEncryptor for FakeEncryptor {
465        fn encrypt(&self, _label: &str, _plaintext: &[u8]) -> Result<Vec<u8>> {
466            Ok(Vec::new())
467        }
468
469        fn decrypt(&self, _label: &str, _ciphertext: &[u8]) -> Result<Vec<u8>> {
470            Ok(Vec::new())
471        }
472    }
473
474    #[test]
475    fn ensure_key_generates_when_missing() {
476        let dir = test_dir();
477        let encryptor = FakeEncryptor::default();
478
479        ensure_key(&encryptor, &dir, "cache-key", AccessPolicy::BiometricOnly).unwrap();
480
481        assert!(encryptor.deleted_labels().is_empty());
482        assert_eq!(
483            encryptor.generated_calls(),
484            vec![(
485                "cache-key".to_string(),
486                KeyType::Encryption,
487                AccessPolicy::BiometricOnly
488            )]
489        );
490
491        std::fs::remove_dir_all(&dir).unwrap();
492    }
493
494    #[test]
495    fn ensure_key_regenerates_when_policy_mismatches() {
496        let dir = test_dir();
497        metadata::save_meta(
498            &dir,
499            "cache-key",
500            &metadata::KeyMeta::new("cache-key", KeyType::Encryption, AccessPolicy::None),
501        )
502        .unwrap();
503        let encryptor = FakeEncryptor::with_existing_key();
504
505        ensure_key(&encryptor, &dir, "cache-key", AccessPolicy::BiometricOnly).unwrap();
506
507        assert_eq!(encryptor.deleted_labels(), vec!["cache-key".to_string()]);
508        assert_eq!(
509            encryptor.generated_calls(),
510            vec![(
511                "cache-key".to_string(),
512                KeyType::Encryption,
513                AccessPolicy::BiometricOnly
514            )]
515        );
516
517        std::fs::remove_dir_all(&dir).unwrap();
518    }
519
520    #[test]
521    fn ensure_key_keeps_existing_key_when_policy_matches() {
522        let dir = test_dir();
523        metadata::save_meta(
524            &dir,
525            "cache-key",
526            &metadata::KeyMeta::new(
527                "cache-key",
528                KeyType::Encryption,
529                AccessPolicy::BiometricOnly,
530            ),
531        )
532        .unwrap();
533        let encryptor = FakeEncryptor::with_existing_key();
534
535        ensure_key(&encryptor, &dir, "cache-key", AccessPolicy::BiometricOnly).unwrap();
536
537        assert!(encryptor.deleted_labels().is_empty());
538        assert!(encryptor.generated_calls().is_empty());
539
540        std::fs::remove_dir_all(&dir).unwrap();
541    }
542}