Skip to main content

hardware_enclave/internal/tpm_bridge/
mod.rs

1// Copyright 2026 Jay Gowdy
2// SPDX-License-Identifier: MIT
3
4//! Generic JSON-RPC TPM bridge server for enclave apps.
5//!
6//! Extracts the common bridge server logic shared by `awsenc-tpm-bridge` and
7//! `sso-jwt-tpm-bridge`. Each app only needs to supply its default app name
8//! and key label, then call [`BridgeServer::run_stdio`].
9//!
10//! # Example
11//!
12//! ```ignore
13//! let mut server = crate::internal::tpm_bridge::BridgeServer::new("myapp", "cache-key");
14//! server.run_stdio().ok();
15//! ```
16#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
17
18mod tpm;
19
20pub use tpm::{TpmSigningStorage, TpmStorage};
21
22use crate::internal::bridge::BridgeResponse;
23use crate::internal::core::types::AccessPolicy;
24use base64::prelude::*;
25use serde::Deserialize;
26use std::io::{self, BufRead, Write};
27
28/// Backward-compatible bridge request that supports both the legacy `biometric`
29/// boolean and the newer `access_policy` enum.
30#[derive(Debug, Deserialize)]
31pub struct BridgeRequestCompat {
32    /// Method: "init", "encrypt", "decrypt", "destroy", "delete".
33    pub method: String,
34    /// Parameters.
35    #[serde(default)]
36    pub params: BridgeParamsCompat,
37}
38
39/// Parameters for a bridge request.
40#[derive(Debug, Default, Deserialize)]
41pub struct BridgeParamsCompat {
42    /// Base64-encoded data (plaintext for encrypt, ciphertext for decrypt).
43    #[serde(default)]
44    pub data: String,
45    /// Access policy to enforce on key use.
46    #[serde(default)]
47    pub access_policy: AccessPolicy,
48    /// Legacy field: older bridge clients send `"biometric": true` instead of
49    /// `"access_policy": "biometric_only"`. Kept for backward compatibility.
50    #[serde(default)]
51    pub biometric: bool,
52    /// Application name (determines TPM key name).
53    #[serde(default)]
54    pub app_name: String,
55    /// Key label within the application namespace.
56    #[serde(default)]
57    pub key_label: String,
58
59    // --- WebAuthn / SK fields. See `crate::internal::bridge::BridgeParams`
60    // for field-by-field semantics. Optional; non-SK methods leave
61    // them empty.
62    #[serde(default)]
63    pub rp_id: Option<String>,
64    #[serde(default)]
65    pub rp_name: Option<String>,
66    #[serde(default)]
67    pub user_id_b64: Option<String>,
68    #[serde(default)]
69    pub user_name: Option<String>,
70    #[serde(default)]
71    pub user_display_name: Option<String>,
72    #[serde(default)]
73    pub credential_id_b64: Option<String>,
74    #[serde(default)]
75    pub client_data_b64: Option<String>,
76    #[serde(default)]
77    pub timeout_ms: Option<u32>,
78}
79
80impl BridgeParamsCompat {
81    /// Return the app name, falling back to the provided default.
82    pub fn app_name_or<'param>(&'param self, default: &'param str) -> &'param str {
83        if self.app_name.is_empty() {
84            default
85        } else {
86            &self.app_name
87        }
88    }
89
90    /// Return the key label, falling back to the provided default.
91    pub fn key_label_or<'param>(&'param self, default: &'param str) -> &'param str {
92        if self.key_label.is_empty() {
93            default
94        } else {
95            &self.key_label
96        }
97    }
98
99    /// Resolve the effective access policy, falling back to the legacy
100    /// `biometric` boolean when `access_policy` is unset (defaults to `None`).
101    pub fn effective_access_policy(&self) -> AccessPolicy {
102        if self.access_policy != AccessPolicy::None {
103            return self.access_policy;
104        }
105        if self.biometric {
106            return AccessPolicy::BiometricOnly;
107        }
108        AccessPolicy::None
109    }
110}
111
112/// Handle a single parsed bridge request, dispatching to the appropriate
113/// TPM storage operation.
114pub fn handle_request(
115    request: &BridgeRequestCompat,
116    storage: &mut Option<TpmStorage>,
117    signing_storage: &mut Option<TpmSigningStorage>,
118    default_app_name: &str,
119    default_key_label: &str,
120) -> BridgeResponse {
121    let app_name = request.params.app_name_or(default_app_name);
122    let key_label = request.params.key_label_or(default_key_label);
123
124    match request.method.as_str() {
125        "init" => {
126            match TpmStorage::new(
127                app_name,
128                key_label,
129                request.params.effective_access_policy(),
130            ) {
131                Ok(s) => {
132                    *storage = Some(s);
133                    BridgeResponse::success("ok")
134                }
135                Err(e) => BridgeResponse::error(&format!("init failed: {e}")),
136            }
137        }
138        "encrypt" => {
139            let Some(ref s) = storage else {
140                return BridgeResponse::error("not initialized: call init first");
141            };
142            if request.params.data.is_empty() {
143                return BridgeResponse::error("missing data parameter");
144            }
145            let plaintext = match BASE64_STANDARD.decode(&request.params.data) {
146                Ok(d) => d,
147                Err(e) => {
148                    return BridgeResponse::error(&format!("base64 decode error: {e}"));
149                }
150            };
151            match s.encrypt(&plaintext) {
152                Ok(ciphertext) => BridgeResponse::success(&BASE64_STANDARD.encode(&ciphertext)),
153                Err(e) => BridgeResponse::error(&format!("encrypt failed: {e}")),
154            }
155        }
156        "decrypt" => {
157            let Some(ref s) = storage else {
158                return BridgeResponse::error("not initialized: call init first");
159            };
160            if request.params.data.is_empty() {
161                return BridgeResponse::error("missing data parameter");
162            }
163            let ciphertext = match BASE64_STANDARD.decode(&request.params.data) {
164                Ok(d) => d,
165                Err(e) => {
166                    return BridgeResponse::error(&format!("base64 decode error: {e}"));
167                }
168            };
169            match s.decrypt(&ciphertext) {
170                Ok(plaintext) => BridgeResponse::success(&BASE64_STANDARD.encode(&plaintext)),
171                Err(e) => BridgeResponse::error(&format!("decrypt failed: {e}")),
172            }
173        }
174        "destroy" | "delete" => match TpmStorage::delete(app_name, key_label) {
175            Ok(()) => {
176                *storage = None;
177                BridgeResponse::success("ok")
178            }
179            Err(e) => BridgeResponse::error(&format!("delete failed: {e}")),
180        },
181        "init_signing" => {
182            match TpmSigningStorage::new(
183                app_name,
184                key_label,
185                request.params.effective_access_policy(),
186            ) {
187                Ok(s) => {
188                    *signing_storage = Some(s);
189                    BridgeResponse::success("ok")
190                }
191                Err(e) => BridgeResponse::error(&format!("init_signing failed: {e}")),
192            }
193        }
194        "sign" => {
195            let Some(ref s) = signing_storage else {
196                return BridgeResponse::error("signing not initialized: call init_signing first");
197            };
198            if request.params.data.is_empty() {
199                return BridgeResponse::error("missing data parameter");
200            }
201            let data = match BASE64_STANDARD.decode(&request.params.data) {
202                Ok(d) => d,
203                Err(e) => {
204                    return BridgeResponse::error(&format!("base64 decode error: {e}"));
205                }
206            };
207            match s.sign(&data) {
208                Ok(signature) => BridgeResponse::success(&BASE64_STANDARD.encode(&signature)),
209                Err(e) => BridgeResponse::error(&format!("sign failed: {e}")),
210            }
211        }
212        "public_key" => {
213            // Standalone: doesn't require prior init_signing. The
214            // previous behaviour required clients to call init_signing
215            // first, which has create-if-missing semantics keyed by
216            // key_label (defaults to "default") -- so any read-only
217            // probe of a non-existent key created a `default` signing
218            // key as a side effect. Same fix shape as `list_keys`
219            // (enclave PR #110): a static `public_key_for_app`
220            // helper that just instantiates a TpmSigner without going
221            // through TpmSigningStorage::new's ensure_signing_key.
222            match TpmSigningStorage::public_key_for_app(app_name, key_label) {
223                Ok(pubkey) => BridgeResponse::success(&BASE64_STANDARD.encode(&pubkey)),
224                Err(e) => BridgeResponse::error(&format!("public_key failed: {e}")),
225            }
226        }
227        "list_keys" => {
228            // Standalone: doesn't require prior init_signing. Otherwise
229            // the client side's call_bridge_after_signing_init would
230            // create a key with the configured key_label (defaults to
231            // "default") as a side effect of every list operation,
232            // which manifested as a "default" key leak in the agent's
233            // identity-enumeration path.
234            match TpmSigningStorage::list_keys_for_app(app_name) {
235                Ok(keys) => {
236                    let json = serde_json::to_string(&keys).unwrap_or_else(|_| "[]".to_string());
237                    BridgeResponse::success(&json)
238                }
239                Err(e) => BridgeResponse::error(&format!("list_keys failed: {e}")),
240            }
241        }
242        "delete_signing" => match TpmSigningStorage::delete(app_name, key_label) {
243            Ok(()) => {
244                *signing_storage = None;
245                BridgeResponse::success("ok")
246            }
247            Err(e) => BridgeResponse::error(&format!("delete_signing failed: {e}")),
248        },
249        // Load-only existence check: returns "true"/"false" and does NOT
250        // create the key. Does not require prior init_signing.
251        // Needed because `init_signing` has load-or-create semantics, so
252        // clients that use public_key/list_keys as an existence test end
253        // up creating the key as a side effect.
254        "signing_key_exists" => match TpmSigningStorage::key_exists(app_name, key_label) {
255            Ok(exists) => BridgeResponse::success(if exists { "true" } else { "false" }),
256            Err(e) => BridgeResponse::error(&format!("signing_key_exists failed: {e}")),
257        },
258        // WebAuthn / SK methods. WSL clients call these to get
259        // hardware-enforced Hello consent on the Windows host
260        // (where webauthn.dll lives). Non-Windows server builds
261        // refuse with a clear error -- those builds shouldn't be
262        // running these methods anyway since the bridge target
263        // platform is Windows.
264        "webauthn_is_available" => webauthn_is_available_handler(),
265        "webauthn_make_credential" => webauthn_make_credential_handler(&request.params),
266        "webauthn_get_assertion" => webauthn_get_assertion_handler(&request.params),
267        "webauthn_delete_platform_credential" => {
268            webauthn_delete_platform_credential_handler(&request.params)
269        }
270        other => BridgeResponse::error(&format!("unknown method: {other}")),
271    }
272}
273
274#[cfg(target_os = "windows")]
275fn webauthn_is_available_handler() -> BridgeResponse {
276    if crate::internal::windows_webauthn::is_platform_authenticator_available() {
277        BridgeResponse::success("true")
278    } else {
279        BridgeResponse::success("false")
280    }
281}
282
283#[cfg(not(target_os = "windows"))]
284fn webauthn_is_available_handler() -> BridgeResponse {
285    BridgeResponse::success("false")
286}
287
288#[cfg(target_os = "windows")]
289#[allow(clippy::too_many_lines)] // tight serial flow, splitting hurts readability
290fn webauthn_make_credential_handler(params: &BridgeParamsCompat) -> BridgeResponse {
291    use crate::internal::windows_webauthn::{make_credential, MakeCredentialParams};
292
293    let Some(rp_id) = params.rp_id.as_deref() else {
294        return BridgeResponse::error("webauthn_make_credential: missing rp_id");
295    };
296    let rp_name = params.rp_name.as_deref().unwrap_or("sshenc");
297    let Some(user_id_b64) = params.user_id_b64.as_deref() else {
298        return BridgeResponse::error("webauthn_make_credential: missing user_id_b64");
299    };
300    let user_id = match BASE64_STANDARD.decode(user_id_b64) {
301        Ok(v) => v,
302        Err(e) => return BridgeResponse::error(&format!("user_id_b64 decode: {e}")),
303    };
304    let user_name = params.user_name.as_deref().unwrap_or("");
305    let user_display_name = params.user_display_name.as_deref().unwrap_or(user_name);
306    let timeout_ms = params.timeout_ms.unwrap_or(60_000);
307
308    match make_credential(MakeCredentialParams {
309        rp_id,
310        rp_name,
311        user_id: &user_id,
312        user_name,
313        user_display_name,
314        timeout_ms,
315        // Bridge process has no console window; let the wrapper
316        // pick GetForegroundWindow / GetDesktopWindow. On a
317        // typical `wsl bash -c "sshenc keygen"` invocation the
318        // foreground window is the Windows terminal hosting WSL.
319        hwnd: None,
320    }) {
321        Ok(cred) => {
322            let payload = crate::internal::bridge::WebauthnMakeCredentialResult {
323                credential_id_b64: BASE64_STANDARD.encode(&cred.credential_id),
324                public_key_x_hex: hex_lower(&cred.public_key_x),
325                public_key_y_hex: hex_lower(&cred.public_key_y),
326                authenticator_data_b64: BASE64_STANDARD.encode(&cred.authenticator_data),
327                resident: cred.resident,
328            };
329            match serde_json::to_string(&payload) {
330                Ok(s) => BridgeResponse::success(&s),
331                Err(e) => BridgeResponse::error(&format!("serialize make_credential result: {e}")),
332            }
333        }
334        Err(e) => BridgeResponse::error(&format!("webauthn_make_credential: {e}")),
335    }
336}
337
338#[cfg(not(target_os = "windows"))]
339fn webauthn_make_credential_handler(_params: &BridgeParamsCompat) -> BridgeResponse {
340    BridgeResponse::error(
341        "webauthn_make_credential: webauthn.dll is only available on Windows; \
342         this bridge build can't service the request",
343    )
344}
345
346#[cfg(target_os = "windows")]
347fn webauthn_get_assertion_handler(params: &BridgeParamsCompat) -> BridgeResponse {
348    use crate::internal::windows_webauthn::{get_assertion, GetAssertionParams};
349
350    let Some(rp_id) = params.rp_id.as_deref() else {
351        return BridgeResponse::error("webauthn_get_assertion: missing rp_id");
352    };
353    let Some(credential_id_b64) = params.credential_id_b64.as_deref() else {
354        return BridgeResponse::error("webauthn_get_assertion: missing credential_id_b64");
355    };
356    let credential_id = match BASE64_STANDARD.decode(credential_id_b64) {
357        Ok(v) => v,
358        Err(e) => return BridgeResponse::error(&format!("credential_id_b64 decode: {e}")),
359    };
360    let Some(client_data_b64) = params.client_data_b64.as_deref() else {
361        return BridgeResponse::error("webauthn_get_assertion: missing client_data_b64");
362    };
363    let client_data = match BASE64_STANDARD.decode(client_data_b64) {
364        Ok(v) => v,
365        Err(e) => return BridgeResponse::error(&format!("client_data_b64 decode: {e}")),
366    };
367    let timeout_ms = params.timeout_ms.unwrap_or(60_000);
368
369    match get_assertion(GetAssertionParams {
370        rp_id,
371        credential_id: &credential_id,
372        client_data: &client_data,
373        timeout_ms,
374        hwnd: None,
375    }) {
376        Ok(asn) => {
377            let payload = crate::internal::bridge::WebauthnAssertionResult {
378                signature_der_b64: BASE64_STANDARD.encode(&asn.signature_der),
379                authenticator_data_b64: BASE64_STANDARD.encode(&asn.authenticator_data),
380                flags: asn.flags,
381                counter: asn.counter,
382            };
383            match serde_json::to_string(&payload) {
384                Ok(s) => BridgeResponse::success(&s),
385                Err(e) => BridgeResponse::error(&format!("serialize assertion result: {e}")),
386            }
387        }
388        Err(e) => BridgeResponse::error(&format!("webauthn_get_assertion: {e}")),
389    }
390}
391
392#[cfg(not(target_os = "windows"))]
393fn webauthn_get_assertion_handler(_params: &BridgeParamsCompat) -> BridgeResponse {
394    BridgeResponse::error(
395        "webauthn_get_assertion: webauthn.dll is only available on Windows; \
396         this bridge build can't service the request",
397    )
398}
399
400#[cfg(target_os = "windows")]
401fn webauthn_delete_platform_credential_handler(params: &BridgeParamsCompat) -> BridgeResponse {
402    let Some(credential_id_b64) = params.credential_id_b64.as_deref() else {
403        return BridgeResponse::error(
404            "webauthn_delete_platform_credential: missing credential_id_b64",
405        );
406    };
407    let credential_id = match BASE64_STANDARD.decode(credential_id_b64) {
408        Ok(v) => v,
409        Err(e) => return BridgeResponse::error(&format!("credential_id_b64 decode: {e}")),
410    };
411    match crate::internal::windows_webauthn::delete_platform_credential(&credential_id) {
412        Ok(()) => BridgeResponse::success("ok"),
413        Err(e) => BridgeResponse::error(&format!("webauthn_delete_platform_credential: {e}")),
414    }
415}
416
417#[cfg(not(target_os = "windows"))]
418fn webauthn_delete_platform_credential_handler(_params: &BridgeParamsCompat) -> BridgeResponse {
419    BridgeResponse::error(
420        "webauthn_delete_platform_credential: webauthn.dll is only available on Windows",
421    )
422}
423
424#[cfg(target_os = "windows")]
425fn hex_lower(bytes: &[u8]) -> String {
426    let mut s = String::with_capacity(bytes.len() * 2);
427    for b in bytes {
428        s.push_str(&format!("{:02x}", b));
429    }
430    s
431}
432
433/// A JSON-RPC bridge server that reads requests from stdin and writes
434/// responses to stdout, delegating to [`TpmStorage`] for crypto operations.
435#[derive(Debug)]
436pub struct BridgeServer {
437    default_app_name: String,
438    default_key_label: String,
439}
440
441impl BridgeServer {
442    /// Create a new bridge server with the given default app name and key label.
443    ///
444    /// These defaults are used when the client omits `app_name` or `key_label`
445    /// from the request parameters.
446    pub fn new(default_app_name: &str, default_key_label: &str) -> Self {
447        Self {
448            default_app_name: default_app_name.to_string(),
449            default_key_label: default_key_label.to_string(),
450        }
451    }
452
453    /// Run the bridge server, reading JSON-RPC requests from stdin and writing
454    /// responses to stdout. Blocks until stdin is closed or a write error
455    /// occurs.
456    #[allow(clippy::print_stdout)]
457    pub fn run_stdio(&mut self) -> io::Result<()> {
458        let stdin = io::stdin();
459        let mut stdout = io::stdout().lock();
460        let mut storage: Option<TpmStorage> = None;
461        let mut signing_storage: Option<TpmSigningStorage> = None;
462
463        for line in stdin.lock().lines() {
464            let line = match line {
465                Ok(l) => l,
466                Err(e) => {
467                    let resp = BridgeResponse::error(&format!("read error: {e}"));
468                    drop(serde_json::to_writer(&mut stdout, &resp));
469                    drop(stdout.write_all(b"\n"));
470                    drop(stdout.flush());
471                    break;
472                }
473            };
474
475            if line.trim().is_empty() {
476                continue;
477            }
478
479            let response = match serde_json::from_str::<BridgeRequestCompat>(&line) {
480                Ok(req) => handle_request(
481                    &req,
482                    &mut storage,
483                    &mut signing_storage,
484                    &self.default_app_name,
485                    &self.default_key_label,
486                ),
487                Err(e) => BridgeResponse::error(&format!("invalid JSON: {e}")),
488            };
489
490            if serde_json::to_writer(&mut stdout, &response).is_err() {
491                break;
492            }
493            if stdout.write_all(b"\n").is_err() {
494                break;
495            }
496            if stdout.flush().is_err() {
497                break;
498            }
499        }
500
501        Ok(())
502    }
503}
504
505#[cfg(test)]
506#[allow(clippy::unwrap_used, clippy::panic)]
507mod tests {
508    use super::*;
509
510    const TEST_APP_NAME: &str = "test-app";
511    const TEST_KEY_LABEL: &str = "cache-key";
512
513    fn make_request(method: &str, data: &str, access_policy: AccessPolicy) -> BridgeRequestCompat {
514        BridgeRequestCompat {
515            method: method.to_string(),
516            params: BridgeParamsCompat {
517                data: data.to_string(),
518                access_policy,
519                biometric: false,
520                app_name: TEST_APP_NAME.to_string(),
521                key_label: TEST_KEY_LABEL.to_string(),
522                ..BridgeParamsCompat::default()
523            },
524        }
525    }
526
527    fn handle(req: &BridgeRequestCompat, storage: &mut Option<TpmStorage>) -> BridgeResponse {
528        let mut signing_storage = None;
529        handle_request(
530            req,
531            storage,
532            &mut signing_storage,
533            TEST_APP_NAME,
534            TEST_KEY_LABEL,
535        )
536    }
537
538    fn handle_signing(
539        req: &BridgeRequestCompat,
540        signing_storage: &mut Option<TpmSigningStorage>,
541    ) -> BridgeResponse {
542        let mut storage = None;
543        handle_request(
544            req,
545            &mut storage,
546            signing_storage,
547            TEST_APP_NAME,
548            TEST_KEY_LABEL,
549        )
550    }
551
552    // ----- Parsing tests -----
553
554    #[test]
555    fn parse_init_request() {
556        let json = r#"{"method": "init", "params": {"access_policy": "none"}}"#;
557        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
558        assert_eq!(req.method, "init");
559        assert_eq!(req.params.access_policy, AccessPolicy::None);
560        assert_eq!(req.params.app_name_or(TEST_APP_NAME), TEST_APP_NAME);
561        assert_eq!(req.params.key_label_or(TEST_KEY_LABEL), TEST_KEY_LABEL);
562    }
563
564    #[test]
565    fn parse_init_request_defaults() {
566        let json = r#"{"method": "init", "params": {}}"#;
567        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
568        assert_eq!(req.method, "init");
569        assert_eq!(req.params.access_policy, AccessPolicy::None);
570        assert!(req.params.data.is_empty());
571        assert_eq!(req.params.app_name_or(TEST_APP_NAME), TEST_APP_NAME);
572        assert_eq!(req.params.key_label_or(TEST_KEY_LABEL), TEST_KEY_LABEL);
573    }
574
575    #[test]
576    fn parse_encrypt_request() {
577        let json =
578            r#"{"method": "encrypt", "params": {"data": "aGVsbG8=", "access_policy": "none"}}"#;
579        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
580        assert_eq!(req.method, "encrypt");
581        assert_eq!(req.params.data, "aGVsbG8=");
582    }
583
584    #[test]
585    fn parse_decrypt_request() {
586        let json = r#"{"method": "decrypt", "params": {"data": "Y2lwaGVy"}}"#;
587        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
588        assert_eq!(req.method, "decrypt");
589        assert_eq!(req.params.data, "Y2lwaGVy");
590    }
591
592    #[test]
593    fn parse_destroy_request() {
594        let json = r#"{"method": "destroy", "params": {}}"#;
595        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
596        assert_eq!(req.method, "destroy");
597        assert_eq!(req.params.app_name_or(TEST_APP_NAME), TEST_APP_NAME);
598        assert_eq!(req.params.key_label_or(TEST_KEY_LABEL), TEST_KEY_LABEL);
599    }
600
601    #[test]
602    fn parse_delete_request() {
603        let json = r#"{"method": "delete", "params": {"key_label": "cache-key"}}"#;
604        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
605        assert_eq!(req.method, "delete");
606        assert_eq!(req.params.key_label_or(TEST_KEY_LABEL), TEST_KEY_LABEL);
607    }
608
609    #[test]
610    fn parse_request_uses_defaults_for_minimal_payloads() {
611        let json = r#"{"method":"init","params":{"access_policy":"biometric_only"}}"#;
612        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
613        assert_eq!(req.params.access_policy, AccessPolicy::BiometricOnly);
614        assert_eq!(req.params.app_name_or(TEST_APP_NAME), TEST_APP_NAME);
615        assert_eq!(req.params.key_label_or(TEST_KEY_LABEL), TEST_KEY_LABEL);
616    }
617
618    #[test]
619    fn parse_request_with_explicit_app_name_and_key_label() {
620        let json = r#"{"method":"init","params":{"app_name":"custom","key_label":"my-key"}}"#;
621        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
622        assert_eq!(req.params.app_name_or(TEST_APP_NAME), "custom");
623        assert_eq!(req.params.key_label_or(TEST_KEY_LABEL), "my-key");
624    }
625
626    // ----- Serialization tests -----
627
628    #[test]
629    fn serialize_success_response() {
630        let resp = BridgeResponse::success("ok");
631        let json = serde_json::to_string(&resp).unwrap();
632        assert!(json.contains("\"result\":\"ok\""));
633    }
634
635    #[test]
636    fn serialize_error_response() {
637        let resp = BridgeResponse::error("something went wrong");
638        let json = serde_json::to_string(&resp).unwrap();
639        assert!(json.contains("\"error\":\"something went wrong\""));
640    }
641
642    // ----- Handler tests -----
643
644    #[test]
645    fn handle_init_creates_storage() {
646        let req = make_request("init", "", AccessPolicy::None);
647        let mut storage = None;
648        let resp = handle(&req, &mut storage);
649        // On non-Windows, init succeeds (stub creates the struct)
650        // but encrypt/decrypt will fail at runtime
651        if let Some(err) = &resp.error {
652            assert!(!err.is_empty(), "init error message should not be empty");
653        } else {
654            assert!(
655                resp.result.is_some(),
656                "init should return a result on success"
657            );
658        }
659    }
660
661    #[test]
662    fn handle_destroy_clears_storage() {
663        let req = make_request("destroy", "", AccessPolicy::None);
664        let mut storage = None;
665        let resp = handle(&req, &mut storage);
666        // On platforms without TPM, destroy may return an error. That's expected.
667        if let Some(err) = &resp.error {
668            assert!(!err.is_empty(), "destroy error message should not be empty");
669        } else {
670            assert!(
671                resp.result.is_some(),
672                "destroy should return a result on success"
673            );
674        }
675        assert!(storage.is_none());
676    }
677
678    #[test]
679    fn handle_delete_clears_storage() {
680        let req = make_request("delete", "", AccessPolicy::None);
681        let mut storage = None;
682        let resp = handle(&req, &mut storage);
683        // On platforms without TPM, delete may return an error. That's expected.
684        if let Some(err) = &resp.error {
685            assert!(!err.is_empty(), "delete error message should not be empty");
686        } else {
687            assert!(
688                resp.result.is_some(),
689                "delete should return a result on success"
690            );
691        }
692        assert!(storage.is_none());
693    }
694
695    #[test]
696    fn destroy_and_delete_are_aliases() {
697        // Locks in the compat guarantee: the bridge server MUST honor
698        // both `destroy` and `delete` on the wire. `bridge_destroy`
699        // on the client side sends `"delete"` for backward
700        // compatibility with older servers, and newer clients may
701        // send either. Both names must produce identical semantics
702        // so neither version drifts into an "unknown method" error.
703        let destroy = make_request("destroy", "", AccessPolicy::None);
704        let delete = make_request("delete", "", AccessPolicy::None);
705
706        let mut storage_a = None;
707        let mut storage_b = None;
708        let resp_destroy = handle(&destroy, &mut storage_a);
709        let resp_delete = handle(&delete, &mut storage_b);
710
711        // Neither response should be the unknown-method sentinel —
712        // that error text is the regression guard.
713        for resp in [&resp_destroy, &resp_delete] {
714            if let Some(err) = &resp.error {
715                assert!(
716                    !err.contains("unknown method"),
717                    "bridge rejected a supported alias as unknown: {err}"
718                );
719            }
720        }
721        // Result shape must match: either both error (no TPM on this
722        // host) or both succeed.
723        assert_eq!(
724            resp_destroy.error.is_some(),
725            resp_delete.error.is_some(),
726            "destroy/delete disagreed: destroy={resp_destroy:?} delete={resp_delete:?}"
727        );
728    }
729
730    #[test]
731    fn handle_unknown_method() {
732        let req = make_request("bogus", "", AccessPolicy::None);
733        let mut storage = None;
734        let resp = handle(&req, &mut storage);
735        assert!(resp
736            .error
737            .as_deref()
738            .is_some_and(|e| e.contains("unknown method")),);
739    }
740
741    #[test]
742    fn handle_encrypt_without_init() {
743        let req = make_request("encrypt", "aGVsbG8=", AccessPolicy::None);
744        let mut storage = None;
745        let resp = handle(&req, &mut storage);
746        assert!(resp
747            .error
748            .as_deref()
749            .is_some_and(|e| e.contains("not initialized")),);
750    }
751
752    #[test]
753    fn handle_decrypt_without_init() {
754        let req = make_request("decrypt", "Y2lwaGVy", AccessPolicy::None);
755        let mut storage = None;
756        let resp = handle(&req, &mut storage);
757        assert!(resp
758            .error
759            .as_deref()
760            .is_some_and(|e| e.contains("not initialized")),);
761    }
762
763    #[test]
764    fn handle_encrypt_missing_data() {
765        let req = make_request("encrypt", "", AccessPolicy::None);
766        // On platforms without a TPM, new() may fail and storage is None,
767        // so we get "not initialized" instead of "missing data". Both are valid errors.
768        let mut storage = TpmStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).ok();
769        let resp = handle(&req, &mut storage);
770        assert!(resp.error.is_some());
771    }
772
773    #[test]
774    fn handle_encrypt_invalid_base64() {
775        let req = make_request("encrypt", "not-valid-base64!!!", AccessPolicy::None);
776        let mut storage = TpmStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).ok();
777        let resp = handle(&req, &mut storage);
778        assert!(resp.error.is_some());
779    }
780
781    #[test]
782    fn handle_decrypt_missing_data() {
783        let req = make_request("decrypt", "", AccessPolicy::None);
784        let mut storage = TpmStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).ok();
785        let resp = handle(&req, &mut storage);
786        assert!(resp.error.is_some());
787    }
788
789    #[cfg(not(target_os = "windows"))]
790    #[test]
791    fn encrypt_returns_platform_error_on_non_windows() {
792        let storage = TpmStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).unwrap();
793        let result = storage.encrypt(b"hello");
794        assert!(result.is_err());
795        assert!(result.unwrap_err().contains("only supported on Windows"));
796    }
797
798    #[cfg(not(target_os = "windows"))]
799    #[test]
800    fn decrypt_returns_platform_error_on_non_windows() {
801        let storage = TpmStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).unwrap();
802        let result = storage.decrypt(b"hello");
803        assert!(result.is_err());
804        assert!(result.unwrap_err().contains("only supported on Windows"));
805    }
806
807    #[test]
808    fn roundtrip_json_protocol() {
809        // Simulate the full JSON protocol flow
810        let init_json = r#"{"method":"init","params":{"app_name":"test-app","key_label":"cache-key","access_policy":"none"}}"#;
811        let encrypt_json = r#"{"method":"encrypt","params":{"data":"aGVsbG8gd29ybGQ=","app_name":"test-app","key_label":"cache-key","access_policy":"none"}}"#;
812        let destroy_json =
813            r#"{"method":"destroy","params":{"app_name":"test-app","key_label":"cache-key"}}"#;
814
815        let mut storage = None;
816        let mut signing_storage = None;
817
818        // Init
819        let req: BridgeRequestCompat = serde_json::from_str(init_json).unwrap();
820        let resp = handle_request(
821            &req,
822            &mut storage,
823            &mut signing_storage,
824            TEST_APP_NAME,
825            TEST_KEY_LABEL,
826        );
827        if let Some(err) = &resp.error {
828            assert!(!err.is_empty(), "init error message should not be empty");
829        } else {
830            assert!(
831                resp.result.is_some(),
832                "init should return a result on success"
833            );
834        }
835
836        // Encrypt (will fail on non-Windows, which is expected)
837        let req: BridgeRequestCompat = serde_json::from_str(encrypt_json).unwrap();
838        let resp = handle_request(
839            &req,
840            &mut storage,
841            &mut signing_storage,
842            TEST_APP_NAME,
843            TEST_KEY_LABEL,
844        );
845        if let Some(err) = &resp.error {
846            assert!(!err.is_empty(), "encrypt error message should not be empty");
847        } else {
848            assert!(
849                resp.result.is_some(),
850                "encrypt should return a result on success"
851            );
852        }
853
854        // Destroy
855        let req: BridgeRequestCompat = serde_json::from_str(destroy_json).unwrap();
856        let resp = handle_request(
857            &req,
858            &mut storage,
859            &mut signing_storage,
860            TEST_APP_NAME,
861            TEST_KEY_LABEL,
862        );
863        if let Some(err) = &resp.error {
864            assert!(!err.is_empty(), "destroy error message should not be empty");
865        } else {
866            assert!(
867                resp.result.is_some(),
868                "destroy should return a result on success",
869            );
870        }
871        assert!(storage.is_none());
872    }
873
874    #[test]
875    fn invalid_json_produces_error() {
876        let bad_json = "this is not json";
877        let result = serde_json::from_str::<BridgeRequestCompat>(bad_json);
878        assert!(result.is_err());
879    }
880
881    // ----- effective_access_policy exhaustive variant tests -----
882
883    #[test]
884    fn effective_access_policy_none_without_biometric() {
885        let json = r#"{"method":"init","params":{}}"#;
886        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
887        assert_eq!(req.params.effective_access_policy(), AccessPolicy::None);
888    }
889
890    #[test]
891    fn effective_access_policy_any() {
892        let json = r#"{"method":"init","params":{"access_policy":"any"}}"#;
893        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
894        assert_eq!(req.params.effective_access_policy(), AccessPolicy::Any);
895    }
896
897    #[test]
898    fn effective_access_policy_password_only() {
899        let json = r#"{"method":"init","params":{"access_policy":"password_only"}}"#;
900        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
901        assert_eq!(
902            req.params.effective_access_policy(),
903            AccessPolicy::PasswordOnly
904        );
905    }
906
907    // ----- Legacy payload defaults -----
908
909    #[test]
910    fn legacy_payload_with_no_params_defaults_to_none() {
911        let json = r#"{"method":"init","params":{}}"#;
912        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
913        assert_eq!(req.params.effective_access_policy(), AccessPolicy::None);
914        assert_eq!(req.params.app_name, "");
915        assert_eq!(req.params.key_label, "");
916    }
917
918    // ----- Biometric and access_policy coexistence -----
919
920    #[test]
921    fn biometric_and_access_policy_coexist_in_json() {
922        let json = r#"{"method":"init","params":{"access_policy":"biometric_only","biometric":true,"app_name":"test","key_label":"k"}}"#;
923        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
924        assert_eq!(
925            req.params.effective_access_policy(),
926            AccessPolicy::BiometricOnly
927        );
928    }
929
930    // ----- Fallback when access_policy absent but biometric true -----
931
932    #[test]
933    fn biometric_true_falls_back_to_biometric_only() {
934        let json =
935            r#"{"method":"init","params":{"biometric":true,"app_name":"a","key_label":"k"}}"#;
936        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
937        assert_eq!(
938            req.params.effective_access_policy(),
939            AccessPolicy::BiometricOnly
940        );
941    }
942
943    #[test]
944    fn legacy_biometric_true_maps_to_biometric_only() {
945        let json = r#"{"method": "init", "params": {"biometric": true}}"#;
946        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
947        assert_eq!(
948            req.params.effective_access_policy(),
949            AccessPolicy::BiometricOnly
950        );
951    }
952
953    #[test]
954    fn legacy_biometric_false_maps_to_none() {
955        let json = r#"{"method": "init", "params": {"biometric": false}}"#;
956        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
957        assert_eq!(req.params.effective_access_policy(), AccessPolicy::None);
958    }
959
960    #[test]
961    fn access_policy_takes_precedence_over_biometric() {
962        // When both fields are present, access_policy wins.
963        let json = r#"{"method": "init", "params": {"access_policy": "any", "biometric": true}}"#;
964        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
965        assert_eq!(req.params.effective_access_policy(), AccessPolicy::Any);
966    }
967
968    #[test]
969    fn password_only_takes_precedence_over_biometric() {
970        let json =
971            r#"{"method":"init","params":{"access_policy":"password_only","biometric":true}}"#;
972        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
973        assert_eq!(
974            req.params.effective_access_policy(),
975            AccessPolicy::PasswordOnly,
976            "explicit access_policy should take precedence over legacy biometric field"
977        );
978    }
979
980    #[test]
981    fn empty_params_all_defaults() {
982        let json = r#"{"method":"init","params":{}}"#;
983        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
984        assert_eq!(req.params.effective_access_policy(), AccessPolicy::None);
985        assert_eq!(req.params.data, "");
986        assert_eq!(req.params.app_name, "");
987        assert_eq!(req.params.key_label, "");
988    }
989
990    // ----- Signing handler tests -----
991
992    #[test]
993    fn handle_init_signing_creates_signing_storage() {
994        let req = make_request("init_signing", "", AccessPolicy::None);
995        let mut signing_storage = None;
996        let resp = handle_signing(&req, &mut signing_storage);
997        if let Some(err) = &resp.error {
998            assert!(!err.is_empty(), "init_signing error should not be empty");
999        } else {
1000            assert!(resp.result.is_some(), "init_signing should return a result");
1001        }
1002    }
1003
1004    #[test]
1005    fn handle_sign_without_init_signing() {
1006        let req = make_request("sign", "aGVsbG8=", AccessPolicy::None);
1007        let mut signing_storage = None;
1008        let resp = handle_signing(&req, &mut signing_storage);
1009        assert!(resp
1010            .error
1011            .as_deref()
1012            .is_some_and(|e| e.contains("signing not initialized")),);
1013    }
1014
1015    #[test]
1016    fn handle_public_key_without_init_signing() {
1017        // public_key is now standalone (parallel of list_keys, see PR
1018        // #110 commentary). Pre-fix it required init_signing first,
1019        // which created a key with the configured key_label as a side
1020        // effect of every read. The handler now uses
1021        // TpmSigningStorage::public_key_for_app, which takes both
1022        // app_name and key_label and returns KeyNotFound when the key
1023        // doesn't exist (instead of creating it).
1024        let req = make_request("public_key", "", AccessPolicy::None);
1025        let mut signing_storage = None;
1026        let resp = handle_signing(&req, &mut signing_storage);
1027        if let Some(err) = &resp.error {
1028            assert!(
1029                !err.contains("signing not initialized"),
1030                "public_key should be standalone, not require init_signing: {err}"
1031            );
1032        }
1033    }
1034
1035    #[test]
1036    fn handle_list_keys_without_init_signing() {
1037        // list_keys is now standalone (doesn't require prior init_signing).
1038        // The previous behavior errored "signing not initialized" but that
1039        // forced clients to call init_signing first, which created a key
1040        // with the configured key_label as a side effect of every list.
1041        // The handler now uses TpmSigningStorage::list_keys_for_app, which
1042        // takes only app_name. On a non-Windows test build the underlying
1043        // call returns "TPM signing bridge is only supported on Windows";
1044        // either way it's NOT the "signing not initialized" sentinel.
1045        let req = make_request("list_keys", "", AccessPolicy::None);
1046        let mut signing_storage = None;
1047        let resp = handle_signing(&req, &mut signing_storage);
1048        if let Some(err) = &resp.error {
1049            assert!(
1050                !err.contains("signing not initialized"),
1051                "list_keys should be standalone, not require init_signing: {err}"
1052            );
1053        }
1054    }
1055
1056    #[test]
1057    fn handle_sign_missing_data() {
1058        let req = make_request("sign", "", AccessPolicy::None);
1059        let mut signing_storage =
1060            TpmSigningStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).ok();
1061        let resp = handle_signing(&req, &mut signing_storage);
1062        assert!(resp.error.is_some());
1063    }
1064
1065    #[test]
1066    fn handle_delete_signing_clears_signing_storage() {
1067        let req = make_request("delete_signing", "", AccessPolicy::None);
1068        let mut signing_storage = None;
1069        let resp = handle_signing(&req, &mut signing_storage);
1070        if let Some(err) = &resp.error {
1071            assert!(
1072                !err.is_empty(),
1073                "delete_signing error message should not be empty"
1074            );
1075        } else {
1076            assert!(
1077                resp.result.is_some(),
1078                "delete_signing should return a result on success"
1079            );
1080        }
1081        assert!(signing_storage.is_none());
1082    }
1083
1084    #[test]
1085    fn parse_init_signing_request() {
1086        let json = r#"{"method": "init_signing", "params": {"access_policy": "none"}}"#;
1087        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
1088        assert_eq!(req.method, "init_signing");
1089        assert_eq!(req.params.access_policy, AccessPolicy::None);
1090    }
1091
1092    #[test]
1093    fn parse_sign_request() {
1094        let json = r#"{"method": "sign", "params": {"data": "aGVsbG8=", "access_policy": "none"}}"#;
1095        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
1096        assert_eq!(req.method, "sign");
1097        assert_eq!(req.params.data, "aGVsbG8=");
1098    }
1099
1100    #[test]
1101    fn parse_public_key_request() {
1102        let json = r#"{"method": "public_key", "params": {}}"#;
1103        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
1104        assert_eq!(req.method, "public_key");
1105    }
1106
1107    #[test]
1108    fn parse_list_keys_request() {
1109        let json = r#"{"method": "list_keys", "params": {}}"#;
1110        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
1111        assert_eq!(req.method, "list_keys");
1112    }
1113
1114    #[test]
1115    fn parse_delete_signing_request() {
1116        let json = r#"{"method": "delete_signing", "params": {"key_label": "cache-key"}}"#;
1117        let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
1118        assert_eq!(req.method, "delete_signing");
1119    }
1120
1121    #[cfg(not(target_os = "windows"))]
1122    #[test]
1123    fn sign_returns_platform_error_on_non_windows() {
1124        let storage =
1125            TpmSigningStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).unwrap();
1126        let result = storage.sign(b"hello");
1127        assert!(result.is_err());
1128        assert!(result.unwrap_err().contains("only supported on Windows"));
1129    }
1130
1131    #[cfg(not(target_os = "windows"))]
1132    #[test]
1133    fn public_key_returns_platform_error_on_non_windows() {
1134        let storage =
1135            TpmSigningStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).unwrap();
1136        let result = storage.public_key();
1137        assert!(result.is_err());
1138        assert!(result.unwrap_err().contains("only supported on Windows"));
1139    }
1140
1141    #[cfg(not(target_os = "windows"))]
1142    #[test]
1143    fn list_keys_returns_platform_error_on_non_windows() {
1144        let storage =
1145            TpmSigningStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).unwrap();
1146        let result = storage.list_keys();
1147        assert!(result.is_err());
1148        assert!(result.unwrap_err().contains("only supported on Windows"));
1149    }
1150
1151    // ----- BridgeServer construction -----
1152
1153    #[test]
1154    fn bridge_server_new() {
1155        let server = BridgeServer::new("myapp", "mykey");
1156        assert_eq!(server.default_app_name, "myapp");
1157        assert_eq!(server.default_key_label, "mykey");
1158    }
1159}