Skip to main content

hardware_enclave/internal/app_storage/
platform.rs

1// Copyright 2026 Jay Gowdy
2// SPDX-License-Identifier: MIT
3
4//! Platform detection and backend identification.
5#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
6
7#[allow(unused_imports)]
8use std::path::PathBuf;
9use zeroize::Zeroizing;
10
11/// Load (or generate-and-persist) the per-app meta-HMAC key from
12/// the platform's native secure store.
13///
14/// - macOS: legacy Keychain via `enclaveapp-apple::meta_hmac`.
15/// - Windows: DPAPI blob under `%APPDATA%\<app>` via
16///   `enclaveapp-windows::meta_hmac`.
17/// - Linux: Secret Service via `enclaveapp-keyring::meta_hmac_key`.
18///
19/// Returns `Some(key)` on success and `None` when the underlying
20/// store is unreachable (Keychain locked, no Secret Service, DPAPI
21/// failure). Production callers should treat `None` the same way:
22/// refuse to persist or load unauthenticated meta. Test paths can
23/// fall back to plain `metadata::save_meta` / `load_meta` when this
24/// returns `None`.
25///
26/// Errors from the platform layer are surfaced (RNG failure, FFI
27/// failure on a store request the caller explicitly initiated). The
28/// "store happens to be locked / not configured" path is always
29/// `Ok(None)` so consumers can branch unconditionally.
30pub fn meta_hmac_key(app_name: &str) -> Option<Zeroizing<Vec<u8>>> {
31    #[cfg(target_os = "macos")]
32    {
33        match crate::internal::apple::meta_hmac::load_or_create(app_name) {
34            Ok(key) => key,
35            Err(e) => {
36                tracing::warn!(
37                    error = %e,
38                    "macOS meta-HMAC key load failed; falling back to no-HMAC mode"
39                );
40                None
41            }
42        }
43    }
44    #[cfg(target_os = "windows")]
45    {
46        match crate::internal::windows::meta_hmac::load_or_create(app_name) {
47            Ok(key) => key,
48            Err(e) => {
49                tracing::warn!(
50                    error = %e,
51                    "Windows DPAPI meta-HMAC key load failed; falling back to no-HMAC mode"
52                );
53                None
54            }
55        }
56    }
57    #[cfg(target_os = "linux")]
58    {
59        crate::internal::keyring::meta_hmac_key(app_name)
60    }
61    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
62    {
63        let _ = app_name;
64        None
65    }
66}
67
68/// Verify the on-disk `<label>.meta` for `app_name` against its
69/// `<label>.meta.hmac` sidecar, auto-migrating a missing sidecar
70/// from the current meta content as a one-shot upgrade path.
71///
72/// Returns:
73/// - `Ok(())` when the sidecar verifies, when the sidecar is
74///   absent and migration writes a fresh one, when the meta file
75///   itself is absent (caller's job to handle key-not-found), or
76///   when the platform store is unreachable (legacy fallback,
77///   matches the existing encryption-side behavior).
78/// - `Err(StorageError::KeyInitFailed)` only on confirmed tamper:
79///   `.meta.hmac` exists but doesn't match the recomputed HMAC of
80///   `.meta`. Caller refuses to use the key.
81///
82/// Designed for callers that don't have an `AppSigningBackend`-
83/// managed lifecycle for the labels they care about (sshenc-agent's
84/// per-label `list`/`sign`/`get` flows). The encryption-side
85/// `AppEncryptionStorage::ensure_key` runs the same logic inline
86/// for its single configured label; this helper is the equivalent
87/// for the signing path's many user-managed labels.
88pub fn verify_meta_integrity(
89    app_name: &str,
90    keys_dir: &std::path::Path,
91    label: &str,
92) -> crate::internal::app_storage::error::Result<()> {
93    // Don't reach into the platform secure store unless there's
94    // actually a `.meta` file to verify. Without this guard, a
95    // synthetic call site (test binary, fresh install probe, dev
96    // tool) hits the macOS Keychain for an item that doesn't exist
97    // yet — which triggers the unsigned-binary ACL prompt to
98    // *create* one and pollutes the user's login keychain with
99    // debris from every distinct binary signature.
100    let meta_path = keys_dir.join(format!("{label}.meta"));
101    if !meta_path.exists() {
102        return Ok(());
103    }
104    let hmac_key = match meta_hmac_key(app_name) {
105        Some(k) => k,
106        None => {
107            // Platform store unreachable — skip verification rather
108            // than refuse to proceed. Matches the legacy "Linux
109            // without Secret Service" fallback. The dispatch layer
110            // already logged a warn.
111            return Ok(());
112        }
113    };
114    match crate::internal::core::metadata::load_meta_with_hmac(
115        keys_dir,
116        label,
117        hmac_key.as_slice(),
118        crate::internal::core::metadata::MetaIntegrityMode::RequireSidecar,
119    ) {
120        Ok(_) => Ok(()),
121        Err(e) => {
122            let msg = e.to_string();
123            if msg.contains(crate::internal::core::metadata::META_HMAC_VERIFY_OP) {
124                return Err(crate::internal::app_storage::error::StorageError::KeyInitFailed(msg));
125            }
126            if msg.contains(crate::internal::core::metadata::META_HMAC_MISSING_OP) {
127                tracing::warn!(
128                    label = %label,
129                    "`.meta.hmac` sidecar missing — migrating from existing meta. \
130                     If you did not just upgrade, treat this as suspicious and \
131                     regenerate the key."
132                );
133                if let Err(migrate_err) = crate::internal::core::metadata::migrate_meta_to_hmac(
134                    keys_dir,
135                    label,
136                    hmac_key.as_slice(),
137                ) {
138                    return Err(
139                        crate::internal::app_storage::error::StorageError::KeyInitFailed(
140                            migrate_err.to_string(),
141                        ),
142                    );
143                }
144                return Ok(());
145            }
146            // Other errors (file missing, deserialize, IO) — the
147            // caller handles them. We don't fail the integrity
148            // check on them; key-not-found is the caller's flow.
149            Ok(())
150        }
151    }
152}
153
154/// Cross-platform per-key meta-integrity verification using the
155/// new keychain-tag trust anchor.
156///
157/// Used by callers that read `.meta` directly from disk and don't
158/// otherwise hit the platform key manager (e.g., sshenc's bulk-list
159/// path that intentionally avoids per-op prompts). On macOS this
160/// pulls the agent-cached meta-HMAC key, recomputes the tag of the
161/// on-disk `.meta`, and compares it to the keychain-stored tag for
162/// the same label.
163///
164/// Returns:
165/// - `Ok(())` on a clean verify, on a missing meta file (caller's
166///   "key not found" flow handles it), and on `KeychainUnavailable`
167///   (fail-open; matches the existing wrapping-key load behavior).
168/// - `Err(StorageError::KeyInitFailed)` on confirmed tamper or
169///   legacy-meta state. Detail string contains the user-actionable
170///   recovery path.
171///
172/// **Platform coverage:** macOS, Windows, and Linux (Secret Service
173/// keyring backend). The Linux TPM backend (`enclaveapp-linux-tpm`)
174/// shares the keyring backend's Secret Service trust domain via
175/// direct dep, so it gets the same trust anchor coverage even
176/// though the Linux TPM doesn't enforce `AccessPolicy` at sign time
177/// — the trust anchor here functions as an integrity check, the
178/// only protection against same-UID `.meta` tamper on a backend
179/// without hardware policy bits.
180pub fn check_meta_integrity(
181    app_name: &str,
182    label: &str,
183    keys_dir: &std::path::Path,
184) -> crate::internal::app_storage::error::Result<()> {
185    // Don't reach into the platform secure store unless an on-disk
186    // `.meta` exists. Without this guard, synthetic call sites (test
187    // binaries, fresh-install probes, rebuilds with new ad-hoc
188    // signatures) trigger the legacy-Keychain ACL prompt for the
189    // unrelated meta-HMAC item from a prior signed binary.
190    let meta_path = keys_dir.join(format!("{label}.meta"));
191    if !meta_path.exists() {
192        return Ok(());
193    }
194    #[cfg(target_os = "macos")]
195    {
196        // Read-only — must NOT trigger a SecItemAdd here. Creation
197        // belongs on the keygen path; the verify-side helper has
198        // to be safe to call from contexts where prompting for a
199        // Keychain ACL would hang (CI runners, locked sessions).
200        let hmac_key = match crate::internal::apple::meta_hmac::load_existing(app_name) {
201            Ok(Some(k)) => k,
202            _ => return Ok(()),
203        };
204        let outcome = crate::internal::apple::meta_tag::verify(
205            app_name,
206            label,
207            keys_dir,
208            hmac_key.as_slice(),
209        )
210        .map_err(|e| {
211            crate::internal::app_storage::error::StorageError::KeyInitFailed(e.to_string())
212        })?;
213        match outcome {
214            crate::internal::apple::meta_tag::VerifyOutcome::Match
215            | crate::internal::apple::meta_tag::VerifyOutcome::NoMeta
216            | crate::internal::apple::meta_tag::VerifyOutcome::KeychainUnavailable => Ok(()),
217            crate::internal::apple::meta_tag::VerifyOutcome::Tamper => Err(
218                crate::internal::app_storage::error::StorageError::KeyInitFailed(format!(
219                    "key '{label}': metadata integrity check failed. The on-disk meta does \
220                     not match the keychain-stored tag — meta may have been tampered with. \
221                     Refusing to proceed. Regenerate the key to restore a known-good state."
222                )),
223            ),
224            crate::internal::apple::meta_tag::VerifyOutcome::Legacy => Err(
225                crate::internal::app_storage::error::StorageError::KeyInitFailed(format!(
226                    "key '{label}' has no integrity tag. This is a one-time migration \
227                     required by upgrading to a build that introduces meta integrity tags, \
228                     and is not something future upgrades will repeat. If you have already \
229                     run `{app_name} migrate-meta` on this machine, treat this as a tamper \
230                     signal — do not run it again. Regenerate the affected key instead. \
231                     Before migrating, verify the key's current policy looks correct: \
232                     `{app_name} inspect {label}`. To migrate: `{app_name} migrate-meta`."
233                )),
234            ),
235        }
236    }
237    #[cfg(target_os = "windows")]
238    {
239        // Read-only — must NOT trigger `CryptProtectData` here.
240        // Creation belongs on the keygen path; the verify-side
241        // helper has to be safe to call from contexts where prompting
242        // for a DPAPI master-key materialization would hang (CI
243        // runners without an interactive desktop, locked sessions).
244        let hmac_key = match crate::internal::windows::meta_hmac::load_existing(app_name) {
245            Ok(Some(k)) => k,
246            _ => return Ok(()),
247        };
248        let outcome = crate::internal::windows::meta_tag::verify(
249            app_name,
250            label,
251            keys_dir,
252            hmac_key.as_slice(),
253        )
254        .map_err(|e| {
255            crate::internal::app_storage::error::StorageError::KeyInitFailed(e.to_string())
256        })?;
257        match outcome {
258            crate::internal::windows::meta_tag::VerifyOutcome::Match
259            | crate::internal::windows::meta_tag::VerifyOutcome::NoMeta
260            | crate::internal::windows::meta_tag::VerifyOutcome::KeychainUnavailable => Ok(()),
261            crate::internal::windows::meta_tag::VerifyOutcome::Tamper => Err(
262                crate::internal::app_storage::error::StorageError::KeyInitFailed(format!(
263                    "key '{label}': metadata integrity check failed. The on-disk meta does \
264                     not match the keychain-stored tag — meta may have been tampered with. \
265                     Refusing to proceed. Regenerate the key to restore a known-good state."
266                )),
267            ),
268            crate::internal::windows::meta_tag::VerifyOutcome::Legacy => Err(
269                crate::internal::app_storage::error::StorageError::KeyInitFailed(format!(
270                    "key '{label}' has no integrity tag. This is a one-time migration \
271                     required by upgrading to a build that introduces meta integrity tags, \
272                     and is not something future upgrades will repeat. If you have already \
273                     run `{app_name} migrate-meta` on this machine, treat this as a tamper \
274                     signal — do not run it again. Regenerate the affected key instead. \
275                     Before migrating, verify the key's current policy looks correct: \
276                     `{app_name} inspect {label}`. To migrate: `{app_name} migrate-meta`."
277                )),
278            ),
279        }
280    }
281    #[cfg(target_os = "linux")]
282    {
283        // Read-only — must NOT trigger a Secret Service write here.
284        // Creation belongs on the keygen path; the verify-side
285        // helper has to be safe to call from contexts where
286        // unlocking a locked keyring would prompt the user (or
287        // silently fail in a non-interactive session).
288        let hmac_key = match crate::internal::keyring::meta_hmac_key_existing(app_name) {
289            Ok(Some(k)) => k,
290            _ => return Ok(()),
291        };
292        let outcome = crate::internal::keyring::meta_tag::verify(
293            app_name,
294            label,
295            keys_dir,
296            hmac_key.as_slice(),
297        )
298        .map_err(|e| {
299            crate::internal::app_storage::error::StorageError::KeyInitFailed(e.to_string())
300        })?;
301        match outcome {
302            crate::internal::keyring::meta_tag::VerifyOutcome::Match
303            | crate::internal::keyring::meta_tag::VerifyOutcome::NoMeta
304            | crate::internal::keyring::meta_tag::VerifyOutcome::KeychainUnavailable => Ok(()),
305            crate::internal::keyring::meta_tag::VerifyOutcome::Tamper => Err(
306                crate::internal::app_storage::error::StorageError::KeyInitFailed(format!(
307                    "key '{label}': metadata integrity check failed. The on-disk meta does \
308                     not match the keychain-stored tag — meta may have been tampered with. \
309                     Refusing to proceed. Regenerate the key to restore a known-good state."
310                )),
311            ),
312            crate::internal::keyring::meta_tag::VerifyOutcome::Legacy => Err(
313                crate::internal::app_storage::error::StorageError::KeyInitFailed(format!(
314                    "key '{label}' has no integrity tag. This is a one-time migration \
315                     required by upgrading to a build that introduces meta integrity tags, \
316                     and is not something future upgrades will repeat. If you have already \
317                     run `{app_name} migrate-meta` on this machine, treat this as a tamper \
318                     signal — do not run it again. Regenerate the affected key instead. \
319                     Before migrating, verify the key's current policy looks correct: \
320                     `{app_name} inspect {label}`. To migrate: `{app_name} migrate-meta`."
321                )),
322            ),
323        }
324    }
325    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
326    {
327        let _ = (app_name, label, keys_dir);
328        Ok(())
329    }
330}
331
332/// Store an HMAC trust anchor for an arbitrary file in the platform
333/// secure store.
334///
335/// `path_label` is a stable identifier derived from the file path
336/// (e.g. a hex SHA-256 of the path bytes from
337/// `enclave::integrity::path_to_label`). On platforms where the
338/// native secure store is unavailable the call silently succeeds
339/// (fail-open) so callers don't need conditional logic.
340pub fn store_file_tag(
341    app_name: &str,
342    path_label: &str,
343    tag: &[u8; 32],
344) -> crate::internal::core::Result<()> {
345    #[cfg(target_os = "macos")]
346    {
347        crate::internal::apple::meta_tag::store(app_name, path_label, tag)
348    }
349    #[cfg(target_os = "windows")]
350    {
351        crate::internal::windows::meta_tag::store(app_name, path_label, tag)
352    }
353    #[cfg(target_os = "linux")]
354    {
355        crate::internal::keyring::meta_tag::store(app_name, path_label, tag)
356    }
357    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
358    {
359        let _ = (app_name, path_label, tag);
360        Ok(())
361    }
362}
363
364/// Load an HMAC trust anchor for an arbitrary file from the platform
365/// secure store.
366///
367/// Returns `Ok(Some(tag))` when the trust anchor is present,
368/// `Ok(None)` when no anchor was found (new file or pre-migration),
369/// and `Err` only for hard platform-store failures. Callers should
370/// treat `Ok(None)` as the legacy / pre-migration path and
371/// `Err` as store unavailable (fail-open).
372pub fn load_file_tag(
373    app_name: &str,
374    path_label: &str,
375) -> crate::internal::core::Result<Option<[u8; 32]>> {
376    #[cfg(target_os = "macos")]
377    {
378        crate::internal::apple::meta_tag::load(app_name, path_label)
379    }
380    #[cfg(target_os = "windows")]
381    {
382        crate::internal::windows::meta_tag::load(app_name, path_label)
383    }
384    #[cfg(target_os = "linux")]
385    {
386        crate::internal::keyring::meta_tag::load(app_name, path_label)
387    }
388    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
389    {
390        let _ = (app_name, path_label);
391        Ok(None)
392    }
393}
394
395/// Delete the HMAC trust anchor for an arbitrary file from the
396/// platform secure store. Idempotent: missing-entry is success.
397pub fn delete_file_tag(app_name: &str, path_label: &str) -> crate::internal::core::Result<()> {
398    #[cfg(target_os = "macos")]
399    {
400        crate::internal::apple::meta_tag::delete(app_name, path_label)
401    }
402    #[cfg(target_os = "windows")]
403    {
404        crate::internal::windows::meta_tag::delete(app_name, path_label)
405    }
406    #[cfg(target_os = "linux")]
407    {
408        crate::internal::keyring::meta_tag::delete(app_name, path_label)
409    }
410    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
411    {
412        let _ = (app_name, path_label);
413        Ok(())
414    }
415}
416
417/// Remove the per-app meta-HMAC key from the platform's secure
418/// store. Used by the uninstall flow so a clean reinstall doesn't
419/// reuse a stale key. Idempotent: missing-entry is success.
420///
421/// Failures are returned to the caller so the uninstall path can
422/// log them; they do not propagate as `Err` from the higher-level
423/// uninstall sequence today, but exposing them here keeps the API
424/// honest.
425pub fn delete_meta_hmac_key(app_name: &str) -> crate::internal::core::Result<()> {
426    #[cfg(target_os = "macos")]
427    {
428        crate::internal::apple::meta_hmac::delete(app_name)
429    }
430    #[cfg(target_os = "windows")]
431    {
432        crate::internal::windows::meta_hmac::delete(app_name)
433    }
434    #[cfg(target_os = "linux")]
435    {
436        // Linux keyring path doesn't expose an explicit delete today;
437        // the keyring entry survives uninstall by design (lets the
438        // user roll their own cleanup with `secret-tool`). If a
439        // delete becomes necessary, add it to enclaveapp-keyring and
440        // dispatch here.
441        let _ = app_name;
442        Ok(())
443    }
444    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
445    {
446        let _ = app_name;
447        Ok(())
448    }
449}
450
451/// Which hardware/software backend is in use.
452#[derive(Debug, Clone, Copy, PartialEq, Eq)]
453pub enum BackendKind {
454    /// macOS Secure Enclave via CryptoKit.
455    SecureEnclave,
456    /// Windows TPM 2.0 via CNG.
457    Tpm,
458    /// Windows per-user DPAPI fallback for VM hosts without TPM 2.0.
459    WindowsDpapi,
460    /// WSL bridge to Windows TPM.
461    TpmBridge,
462    /// Keyring-backed P-256 keys (Linux without TPM).
463    Keyring,
464}
465
466impl std::fmt::Display for BackendKind {
467    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
468        match self {
469            BackendKind::SecureEnclave => write!(f, "Secure Enclave"),
470            BackendKind::Tpm => write!(f, "TPM 2.0"),
471            BackendKind::WindowsDpapi => write!(f, "Windows DPAPI"),
472            BackendKind::TpmBridge => write!(f, "TPM 2.0 (WSL Bridge)"),
473            BackendKind::Keyring => write!(f, "Keyring"),
474        }
475    }
476}
477
478/// Search for a WSL TPM bridge executable.
479///
480/// Tries in order:
481/// 1. `crate::internal::bridge::find_bridge(app_name)` (standard enclave discovery)
482/// 2. Auto-derived paths: `/mnt/c/Program Files/{app_name}/{app_name}-tpm-bridge.exe`
483///    and `/mnt/c/ProgramData/{app_name}/{app_name}-tpm-bridge.exe`
484/// 3. Any absolute extra paths provided by the caller as explicit overrides
485#[cfg(target_os = "linux")]
486pub fn find_bridge_executable(app_name: &str, extra_paths: &[String]) -> Option<PathBuf> {
487    // Standard enclave discovery.
488    if let Some(path) = crate::internal::bridge::find_bridge(app_name) {
489        return Some(path);
490    }
491
492    // Auto-derived paths.
493    let auto_paths = [
494        format!("/mnt/c/Program Files/{app_name}/{app_name}-tpm-bridge.exe"),
495        format!("/mnt/c/ProgramData/{app_name}/{app_name}-tpm-bridge.exe"),
496    ];
497
498    for path_str in &auto_paths {
499        let path = std::path::Path::new(path_str);
500        if path.exists() {
501            return Some(path.to_path_buf());
502        }
503    }
504
505    for path_str in extra_paths {
506        let path = std::path::Path::new(path_str);
507        if path.is_absolute() && path.exists() {
508            return Some(path.to_path_buf());
509        }
510    }
511
512    None
513}
514
515#[cfg(test)]
516#[allow(clippy::unwrap_used)]
517mod tests {
518    use super::*;
519
520    #[test]
521    fn backend_kind_display() {
522        assert_eq!(BackendKind::SecureEnclave.to_string(), "Secure Enclave");
523        assert_eq!(BackendKind::Tpm.to_string(), "TPM 2.0");
524        assert_eq!(BackendKind::WindowsDpapi.to_string(), "Windows DPAPI");
525        assert_eq!(BackendKind::TpmBridge.to_string(), "TPM 2.0 (WSL Bridge)");
526        assert_eq!(BackendKind::Keyring.to_string(), "Keyring");
527    }
528
529    #[test]
530    fn backend_kind_eq() {
531        assert_eq!(BackendKind::SecureEnclave, BackendKind::SecureEnclave);
532        assert_ne!(BackendKind::SecureEnclave, BackendKind::Tpm);
533    }
534
535    #[test]
536    fn backend_kind_clone() {
537        let kind = BackendKind::Tpm;
538        let cloned = kind;
539        assert_eq!(kind, cloned);
540    }
541
542    #[test]
543    fn backend_kind_debug_nonempty() {
544        for kind in [
545            BackendKind::SecureEnclave,
546            BackendKind::Tpm,
547            BackendKind::WindowsDpapi,
548            BackendKind::TpmBridge,
549            BackendKind::Keyring,
550        ] {
551            let s = format!("{kind:?}");
552            assert!(!s.is_empty());
553        }
554    }
555
556    #[test]
557    fn backend_kind_all_variants_display_nonempty() {
558        for kind in [
559            BackendKind::SecureEnclave,
560            BackendKind::Tpm,
561            BackendKind::WindowsDpapi,
562            BackendKind::TpmBridge,
563            BackendKind::Keyring,
564        ] {
565            assert!(!kind.to_string().is_empty());
566        }
567    }
568
569    #[test]
570    fn backend_kind_all_pairs_not_equal() {
571        let kinds = [
572            BackendKind::SecureEnclave,
573            BackendKind::Tpm,
574            BackendKind::WindowsDpapi,
575            BackendKind::TpmBridge,
576            BackendKind::Keyring,
577        ];
578        for i in 0..kinds.len() {
579            for j in 0..kinds.len() {
580                if i != j {
581                    assert_ne!(kinds[i], kinds[j]);
582                }
583            }
584        }
585    }
586
587    #[test]
588    fn verify_meta_integrity_succeeds_when_meta_file_absent() {
589        let dir = std::env::temp_dir().join(format!(
590            "enclaveapp-platform-test-{}-{}",
591            std::process::id(),
592            std::time::SystemTime::now()
593                .duration_since(std::time::UNIX_EPOCH)
594                .unwrap_or_default()
595                .as_nanos()
596        ));
597        std::fs::create_dir_all(&dir).unwrap();
598        // No .meta file — verify_meta_integrity must return Ok without touching any secure store.
599        let result = verify_meta_integrity("test-app", &dir, "nonexistent-label");
600        assert!(result.is_ok());
601        drop(std::fs::remove_dir_all(&dir));
602    }
603
604    #[test]
605    fn check_meta_integrity_succeeds_when_meta_file_absent() {
606        let dir = std::env::temp_dir().join(format!(
607            "enclaveapp-platform-test2-{}-{}",
608            std::process::id(),
609            std::time::SystemTime::now()
610                .duration_since(std::time::UNIX_EPOCH)
611                .unwrap_or_default()
612                .as_nanos()
613        ));
614        std::fs::create_dir_all(&dir).unwrap();
615        let result = check_meta_integrity("test-app", "nonexistent-label", &dir);
616        assert!(result.is_ok());
617        drop(std::fs::remove_dir_all(&dir));
618    }
619
620    #[cfg(target_os = "linux")]
621    #[test]
622    fn find_bridge_executable_returns_none_on_dev_machine() {
623        // Should return None on most dev machines (not WSL with bridge installed).
624        drop(find_bridge_executable("test-app", &[]));
625    }
626}