#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
use super::protocol::*;
use crate::internal::core::timeout::{wait_with_timeout, LineReaderWithTimeout, TimeoutResult};
use crate::internal::core::{AccessPolicy, Result};
use std::collections::HashMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
const MAX_BRIDGE_RESPONSE_BYTES: usize = 64 * 1024;
const DEFAULT_BRIDGE_REQUEST_TIMEOUT: Duration = Duration::from_secs(240);
const BRIDGE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
fn bridge_request_timeout() -> Duration {
std::env::var("ENCLAVEAPP_BRIDGE_TIMEOUT_SECS")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.map(Duration::from_secs)
.unwrap_or(DEFAULT_BRIDGE_REQUEST_TIMEOUT)
}
#[allow(clippy::print_stderr)] pub fn find_bridge(app_name: &str) -> Option<PathBuf> {
let app_specific = format!("{}_BRIDGE_PATH", app_name.to_uppercase().replace('-', "_"));
for var in [app_specific.as_str(), "ENCLAVEAPP_BRIDGE_PATH"] {
if let Ok(value) = std::env::var(var) {
let p = PathBuf::from(&value);
if p.exists() {
return Some(p);
}
eprintln!(
"warning: {var}={value} is set but the file does not exist; falling back to auto-discovery"
);
}
}
let candidates = [
format!("/mnt/c/Program Files/{app_name}/{app_name}-tpm-bridge.exe"),
format!("/mnt/c/ProgramData/{app_name}/{app_name}-tpm-bridge.exe"),
format!("/mnt/c/Program Files/{app_name}/{app_name}-bridge.exe"),
format!("/mnt/c/ProgramData/{app_name}/{app_name}-bridge.exe"),
];
candidates
.iter()
.filter_map(|path| {
let p = PathBuf::from(path);
let mtime = std::fs::metadata(&p).ok()?.modified().ok()?;
Some((p, mtime))
})
.max_by_key(|(_, mtime)| *mtime)
.map(|(path, _)| path)
}
const BUILD_REQUIRES_SIGNED: bool = option_env!("ENCLAVEAPP_BRIDGE_REQUIRE_SIGNED").is_some();
pub fn require_bridge_is_authenticode_signed(bridge_path: &Path) -> Result<()> {
check_bridge_signature(bridge_path, BUILD_REQUIRES_SIGNED)
}
fn check_bridge_signature(bridge_path: &Path, require: bool) -> Result<()> {
if !require {
return Ok(());
}
let is_exe = bridge_path
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| e.eq_ignore_ascii_case("exe"));
if !is_exe {
return Ok(());
}
match pe_has_authenticode_table(bridge_path) {
Ok(true) => Ok(()),
Ok(false) => Err(crate::internal::core::Error::KeyOperation {
operation: "bridge_verify".into(),
detail: format!(
"bridge binary at {} has no Authenticode signature; refusing to spawn. \
This build was compiled with ENCLAVEAPP_BRIDGE_REQUIRE_SIGNED=1; \
reinstall from a signed release or recompile without that flag.",
bridge_path.display()
),
}),
Err(detail) => Err(crate::internal::core::Error::KeyOperation {
operation: "bridge_verify".into(),
detail,
}),
}
}
fn pe_has_authenticode_table(path: &Path) -> std::result::Result<bool, String> {
let data = std::fs::read(path).map_err(|e| format!("read {}: {e}", path.display()))?;
if data.len() < 0x40 {
return Err("file too small for DOS header".into());
}
if &data[0..2] != b"MZ" {
return Err("not a PE image (missing MZ magic)".into());
}
let e_lfanew = u32::from_le_bytes(
data[0x3C..0x40]
.try_into()
.map_err(|_| "bad e_lfanew slice".to_string())?,
) as usize;
if data.len() < e_lfanew + 0x18 {
return Err("file too small for NT header".into());
}
if &data[e_lfanew..e_lfanew + 4] != b"PE\0\0" {
return Err("not a PE image (missing PE signature)".into());
}
let coff_start = e_lfanew + 4;
let opt_header_start = coff_start + 20;
if data.len() < opt_header_start + 2 {
return Err("file too small for optional header magic".into());
}
let magic = u16::from_le_bytes(
data[opt_header_start..opt_header_start + 2]
.try_into()
.map_err(|_| "bad optional-header magic slice".to_string())?,
);
let data_dir_offset = match magic {
0x10b => 96, 0x20b => 112, _ => return Err(format!("unknown PE optional-header magic 0x{magic:x}")),
};
let security_dir = opt_header_start + data_dir_offset + 4 * 8;
if data.len() < security_dir + 8 {
return Err("file too small for data directory table".into());
}
let size = u32::from_le_bytes(
data[security_dir + 4..security_dir + 8]
.try_into()
.map_err(|_| "bad security dir size slice".to_string())?,
);
Ok(size > 0)
}
struct BridgeSession {
child: std::process::Child,
reader: LineReaderWithTimeout,
finished: bool,
request_timeout: Duration,
}
impl BridgeSession {
fn spawn(bridge_path: &Path) -> Result<Self> {
require_bridge_is_authenticode_signed(bridge_path)?;
let mut child = Command::new(bridge_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.map_err(|e| crate::internal::core::Error::KeyOperation {
operation: "bridge_spawn".into(),
detail: e.to_string(),
})?;
let stdout =
child
.stdout
.take()
.ok_or_else(|| crate::internal::core::Error::KeyOperation {
operation: "bridge_read".into(),
detail: "no stdout".into(),
})?;
Ok(Self {
child,
reader: LineReaderWithTimeout::with_max_line_bytes(stdout, MAX_BRIDGE_RESPONSE_BYTES),
finished: false,
request_timeout: bridge_request_timeout(),
})
}
fn request(&mut self, request: &BridgeRequest) -> Result<BridgeResponse> {
let request_json = serde_json::to_string(request)
.map_err(|e| crate::internal::core::Error::Serialization(e.to_string()))?;
if let Some(ref mut stdin) = self.child.stdin {
writeln!(stdin, "{request_json}").map_err(crate::internal::core::Error::Io)?;
stdin.flush().map_err(crate::internal::core::Error::Io)?;
}
let line = match self.reader.recv_line(self.request_timeout) {
TimeoutResult::Completed(Ok(line)) => line,
TimeoutResult::Completed(Err(e)) => {
let cap_hit = e.kind() == std::io::ErrorKind::InvalidData;
drop(self.child.kill());
drop(self.child.wait());
self.finished = true;
return if cap_hit {
Err(crate::internal::core::Error::KeyOperation {
operation: "bridge_read".into(),
detail: format!(
"bridge response exceeded {MAX_BRIDGE_RESPONSE_BYTES}-byte cap \
before newline ({e})"
),
})
} else {
Err(crate::internal::core::Error::Io(e))
};
}
TimeoutResult::TimedOut => {
drop(self.child.kill());
drop(self.child.wait());
self.finished = true;
return Err(crate::internal::core::Error::KeyOperation {
operation: "bridge_read".into(),
detail: format!(
"bridge did not respond within {}s (set ENCLAVEAPP_BRIDGE_TIMEOUT_SECS to override)",
self.request_timeout.as_secs()
),
});
}
};
if line.trim().is_empty() {
return Err(crate::internal::core::Error::KeyOperation {
operation: "bridge_read".into(),
detail: "bridge returned no response".into(),
});
}
let response: BridgeResponse = serde_json::from_str(&line).map_err(|e| {
crate::internal::core::Error::Serialization(format!("bridge response: {e}"))
})?;
Ok(response)
}
}
impl Drop for BridgeSession {
fn drop(&mut self) {
if self.finished {
return;
}
drop(self.child.stdin.take());
match wait_with_timeout(&mut self.child, BRIDGE_SHUTDOWN_TIMEOUT) {
Ok(TimeoutResult::Completed(_)) => {
self.finished = true;
return;
}
Ok(TimeoutResult::TimedOut) | Err(_) => {
}
}
drop(self.child.kill());
drop(self.child.wait());
self.finished = true;
}
}
type PersistentSessionMap = HashMap<PathBuf, Arc<Mutex<Option<BridgeSession>>>>;
static PERSISTENT_BRIDGES: OnceLock<Mutex<PersistentSessionMap>> = OnceLock::new();
fn persistent_session(bridge_path: &Path) -> Arc<Mutex<Option<BridgeSession>>> {
let map = PERSISTENT_BRIDGES.get_or_init(|| Mutex::new(HashMap::new()));
let mut guard = match map.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
guard
.entry(bridge_path.to_path_buf())
.or_insert_with(|| Arc::new(Mutex::new(None)))
.clone()
}
fn lock_session(
arc: &Arc<Mutex<Option<BridgeSession>>>,
) -> std::sync::MutexGuard<'_, Option<BridgeSession>> {
match arc.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
}
}
pub fn call_bridge(bridge_path: &Path, request: &BridgeRequest) -> Result<BridgeResponse> {
let session_arc = persistent_session(bridge_path);
let mut guard = lock_session(&session_arc);
let mut last_err: Option<crate::internal::core::Error> = None;
for _attempt in 0..2 {
if guard.is_none() {
*guard = Some(BridgeSession::spawn(bridge_path)?);
}
match guard.as_mut().expect("just-spawned").request(request) {
Ok(resp) => return Ok(resp),
Err(e) => {
drop(guard.take());
last_err = Some(e);
}
}
}
Err(last_err.expect("retry loop never recorded an error"))
}
fn init_request(app_name: &str, key_label: &str, access_policy: AccessPolicy) -> BridgeRequest {
BridgeRequest {
method: "init".to_string(),
params: BridgeParams::new(
String::new(),
access_policy,
app_name.to_string(),
key_label.to_string(),
),
}
}
fn call_bridge_after_init(
bridge_path: &Path,
app_name: &str,
key_label: &str,
access_policy: AccessPolicy,
request: &BridgeRequest,
) -> Result<BridgeResponse> {
let session_arc = persistent_session(bridge_path);
let mut guard = lock_session(&session_arc);
let mut last_err: Option<crate::internal::core::Error> = None;
for _attempt in 0..2 {
if guard.is_none() {
*guard = Some(BridgeSession::spawn(bridge_path)?);
}
let session = guard.as_mut().expect("just-spawned");
let result = (|| -> Result<BridgeResponse> {
session
.request(&init_request(app_name, key_label, access_policy))?
.require_ok("bridge_init")?;
session.request(request)
})();
match result {
Ok(resp) => return Ok(resp),
Err(e) => {
drop(guard.take());
last_err = Some(e);
}
}
}
Err(last_err.expect("retry loop never recorded an error"))
}
pub fn bridge_init(
bridge_path: &Path,
app_name: &str,
key_label: &str,
access_policy: AccessPolicy,
) -> Result<()> {
let response = call_bridge(
bridge_path,
&init_request(app_name, key_label, access_policy),
)?;
response.require_ok("bridge_init")
}
pub fn bridge_encrypt(
bridge_path: &Path,
app_name: &str,
key_label: &str,
plaintext: &[u8],
access_policy: AccessPolicy,
) -> Result<Vec<u8>> {
let request = BridgeRequest {
method: "encrypt".to_string(),
params: BridgeParams::new(
encode_data(plaintext),
access_policy,
app_name.to_string(),
key_label.to_string(),
),
};
let response =
call_bridge_after_init(bridge_path, app_name, key_label, access_policy, &request)?;
response.decode_result("bridge_encrypt")
}
pub fn bridge_decrypt(
bridge_path: &Path,
app_name: &str,
key_label: &str,
ciphertext: &[u8],
access_policy: AccessPolicy,
) -> Result<Vec<u8>> {
let request = BridgeRequest {
method: "decrypt".to_string(),
params: BridgeParams::new(
encode_data(ciphertext),
access_policy,
app_name.to_string(),
key_label.to_string(),
),
};
let response =
call_bridge_after_init(bridge_path, app_name, key_label, access_policy, &request)?;
response.decode_result("bridge_decrypt")
}
pub fn bridge_destroy(bridge_path: &Path, app_name: &str, key_label: &str) -> Result<()> {
let request = BridgeRequest {
method: "delete".to_string(),
params: BridgeParams::new(
String::new(),
AccessPolicy::None,
app_name.to_string(),
key_label.to_string(),
),
};
let response = call_bridge(bridge_path, &request)?;
response.require_ok("bridge_destroy")
}
pub fn bridge_delete(bridge_path: &Path, app_name: &str, key_label: &str) -> Result<()> {
bridge_destroy(bridge_path, app_name, key_label)
}
fn init_signing_request(
app_name: &str,
key_label: &str,
access_policy: AccessPolicy,
) -> BridgeRequest {
BridgeRequest {
method: "init_signing".to_string(),
params: BridgeParams::new(
String::new(),
access_policy,
app_name.to_string(),
key_label.to_string(),
),
}
}
fn call_bridge_after_signing_init(
bridge_path: &Path,
app_name: &str,
key_label: &str,
access_policy: AccessPolicy,
request: &BridgeRequest,
) -> Result<BridgeResponse> {
let session_arc = persistent_session(bridge_path);
let mut guard = lock_session(&session_arc);
let mut last_err: Option<crate::internal::core::Error> = None;
for _attempt in 0..2 {
if guard.is_none() {
*guard = Some(BridgeSession::spawn(bridge_path)?);
}
let session = guard.as_mut().expect("just-spawned");
let result = (|| -> Result<BridgeResponse> {
session
.request(&init_signing_request(app_name, key_label, access_policy))?
.require_ok("bridge_init_signing")?;
session.request(request)
})();
match result {
Ok(resp) => return Ok(resp),
Err(e) => {
drop(guard.take());
last_err = Some(e);
}
}
}
Err(last_err.expect("retry loop never recorded an error"))
}
pub fn bridge_init_signing(
bridge_path: &Path,
app_name: &str,
key_label: &str,
access_policy: AccessPolicy,
) -> Result<()> {
let response = call_bridge(
bridge_path,
&init_signing_request(app_name, key_label, access_policy),
)?;
response.require_ok("bridge_init_signing")
}
pub fn bridge_sign(
bridge_path: &Path,
app_name: &str,
key_label: &str,
data: &[u8],
access_policy: AccessPolicy,
) -> Result<Vec<u8>> {
let request = BridgeRequest {
method: "sign".to_string(),
params: BridgeParams::new(
encode_data(data),
access_policy,
app_name.to_string(),
key_label.to_string(),
),
};
let response =
call_bridge_after_signing_init(bridge_path, app_name, key_label, access_policy, &request)?;
response.decode_result("bridge_sign")
}
pub fn bridge_public_key(
bridge_path: &Path,
app_name: &str,
key_label: &str,
access_policy: AccessPolicy,
) -> Result<Vec<u8>> {
let request = BridgeRequest {
method: "public_key".to_string(),
params: BridgeParams::new(
String::new(),
access_policy,
app_name.to_string(),
key_label.to_string(),
),
};
let response = call_bridge(bridge_path, &request)?;
response.decode_result("bridge_public_key")
}
pub fn bridge_list_keys(
bridge_path: &Path,
app_name: &str,
_key_label: &str,
access_policy: AccessPolicy,
) -> Result<Vec<String>> {
let request = BridgeRequest {
method: "list_keys".to_string(),
params: BridgeParams::new(
String::new(),
access_policy,
app_name.to_string(),
String::new(),
),
};
let response = call_bridge(bridge_path, &request)?;
let result_str = response.require_result("bridge_list_keys")?;
serde_json::from_str(result_str)
.map_err(|e| crate::internal::core::Error::Serialization(format!("list_keys JSON: {e}")))
}
pub fn bridge_delete_signing(bridge_path: &Path, app_name: &str, key_label: &str) -> Result<()> {
let request = BridgeRequest {
method: "delete_signing".to_string(),
params: BridgeParams::new(
String::new(),
AccessPolicy::None,
app_name.to_string(),
key_label.to_string(),
),
};
let response = call_bridge(bridge_path, &request)?;
response.require_ok("bridge_delete_signing")
}
pub fn bridge_signing_key_exists(
bridge_path: &Path,
app_name: &str,
key_label: &str,
) -> Result<bool> {
let request = BridgeRequest {
method: "signing_key_exists".to_string(),
params: BridgeParams::new(
String::new(),
AccessPolicy::None,
app_name.to_string(),
key_label.to_string(),
),
};
let response = call_bridge(bridge_path, &request)?;
let result = response.require_result("bridge_signing_key_exists")?;
match result {
"true" => Ok(true),
"false" => Ok(false),
other => Err(crate::internal::core::Error::KeyOperation {
operation: "bridge_signing_key_exists".into(),
detail: format!("unexpected result: {other}"),
}),
}
}
pub fn bridge_webauthn_is_available(bridge_path: &Path) -> Result<bool> {
let request = BridgeRequest {
method: "webauthn_is_available".to_string(),
params: BridgeParams::default(),
};
let response = call_bridge(bridge_path, &request)?;
let result = response.require_result("bridge_webauthn_is_available")?;
Ok(result == "true")
}
#[allow(clippy::too_many_arguments)]
pub fn bridge_webauthn_make_credential(
bridge_path: &Path,
rp_id: &str,
rp_name: &str,
user_id: &[u8],
user_name: &str,
user_display_name: &str,
timeout_ms: u32,
) -> Result<WebauthnMakeCredentialResult> {
let params = BridgeParams {
rp_id: Some(rp_id.to_string()),
rp_name: Some(rp_name.to_string()),
user_id_b64: Some(encode_data(user_id)),
user_name: Some(user_name.to_string()),
user_display_name: Some(user_display_name.to_string()),
timeout_ms: Some(timeout_ms),
..BridgeParams::default()
};
let request = BridgeRequest {
method: "webauthn_make_credential".to_string(),
params,
};
let response = call_bridge(bridge_path, &request)?;
let result_json = response.require_result("bridge_webauthn_make_credential")?;
serde_json::from_str(result_json).map_err(|e| crate::internal::core::Error::KeyOperation {
operation: "bridge_webauthn_make_credential".into(),
detail: format!("failed to parse make_credential result: {e}"),
})
}
pub fn bridge_webauthn_get_assertion(
bridge_path: &Path,
rp_id: &str,
credential_id: &[u8],
client_data: &[u8],
timeout_ms: u32,
) -> Result<WebauthnAssertionResult> {
let params = BridgeParams {
rp_id: Some(rp_id.to_string()),
credential_id_b64: Some(encode_data(credential_id)),
client_data_b64: Some(encode_data(client_data)),
timeout_ms: Some(timeout_ms),
..BridgeParams::default()
};
let request = BridgeRequest {
method: "webauthn_get_assertion".to_string(),
params,
};
let response = call_bridge(bridge_path, &request)?;
let result_json = response.require_result("bridge_webauthn_get_assertion")?;
serde_json::from_str(result_json).map_err(|e| crate::internal::core::Error::KeyOperation {
operation: "bridge_webauthn_get_assertion".into(),
detail: format!("failed to parse get_assertion result: {e}"),
})
}
pub fn bridge_webauthn_delete_platform_credential(
bridge_path: &Path,
credential_id: &[u8],
) -> Result<()> {
let params = BridgeParams {
credential_id_b64: Some(encode_data(credential_id)),
..BridgeParams::default()
};
let request = BridgeRequest {
method: "webauthn_delete_platform_credential".to_string(),
params,
};
let response = call_bridge(bridge_path, &request)?;
response.require_ok("bridge_webauthn_delete_platform_credential")
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
#[cfg(unix)]
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(unix)]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(unix)]
use std::sync::Mutex;
#[cfg(unix)]
static SCRIPT_COUNTER: AtomicU64 = AtomicU64::new(0);
#[cfg(unix)]
static SCRIPT_TEST_MUTEX: Mutex<()> = Mutex::new(());
#[cfg(unix)]
fn temp_script(name: &str, body: &str) -> PathBuf {
let id = SCRIPT_COUNTER.fetch_add(1, Ordering::SeqCst);
let path = std::env::temp_dir().join(format!(
"enclaveapp-bridge-test-{}-{}-{}",
std::process::id(),
id,
name
));
fs::write(&path, body).unwrap();
let mut perms = fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms).unwrap();
path
}
#[cfg(unix)]
fn cleanup_script(path: &Path) {
drop(fs::remove_file(path));
}
#[cfg(unix)]
#[test]
fn find_bridge_returns_none_when_not_found() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
std::env::remove_var("ENCLAVEAPP_BRIDGE_PATH");
std::env::remove_var("ENCLAVEAPP_NONEXISTENT_TEST_APP_BRIDGE_PATH");
let result = find_bridge("enclaveapp-nonexistent-test-app");
assert!(result.is_none(), "expected None, got: {result:?}");
}
#[cfg(unix)]
fn make_pe_bytes(security_size: u32, magic: u16) -> Vec<u8> {
let opt_header_size: usize = if magic == 0x10b {
96 + 16 * 8
} else {
112 + 16 * 8
};
let total = 0x40 + 4 + 20 + opt_header_size;
let mut buf = vec![0_u8; total];
buf[0] = b'M';
buf[1] = b'Z';
buf[0x3C..0x40].copy_from_slice(&0x40_u32.to_le_bytes());
buf[0x40] = b'P';
buf[0x41] = b'E';
let opt_header_start = 0x58;
buf[opt_header_start..opt_header_start + 2].copy_from_slice(&magic.to_le_bytes());
let data_dir_offset = if magic == 0x10b { 96 } else { 112 };
let security_dir = opt_header_start + data_dir_offset + 4 * 8;
buf[security_dir..security_dir + 4].copy_from_slice(&0_u32.to_le_bytes());
buf[security_dir + 4..security_dir + 8].copy_from_slice(&security_size.to_le_bytes());
buf
}
#[cfg(unix)]
#[test]
fn pe_has_authenticode_table_detects_signed_pe32() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let bytes = make_pe_bytes(1024, 0x10b);
let path = std::env::temp_dir().join(format!(
"enclaveapp-pe-signed-{}-{}.exe",
std::process::id(),
SCRIPT_COUNTER.fetch_add(1, Ordering::SeqCst)
));
fs::write(&path, &bytes).unwrap();
assert!(pe_has_authenticode_table(&path).unwrap());
drop(fs::remove_file(&path));
}
#[cfg(unix)]
#[test]
fn pe_has_authenticode_table_detects_signed_pe32plus() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let bytes = make_pe_bytes(4096, 0x20b);
let path = std::env::temp_dir().join(format!(
"enclaveapp-pe-signed64-{}-{}.exe",
std::process::id(),
SCRIPT_COUNTER.fetch_add(1, Ordering::SeqCst)
));
fs::write(&path, &bytes).unwrap();
assert!(pe_has_authenticode_table(&path).unwrap());
drop(fs::remove_file(&path));
}
#[cfg(unix)]
#[test]
fn pe_has_authenticode_table_detects_unsigned() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let bytes = make_pe_bytes(0, 0x10b);
let path = std::env::temp_dir().join(format!(
"enclaveapp-pe-unsigned-{}-{}.exe",
std::process::id(),
SCRIPT_COUNTER.fetch_add(1, Ordering::SeqCst)
));
fs::write(&path, &bytes).unwrap();
assert!(!pe_has_authenticode_table(&path).unwrap());
drop(fs::remove_file(&path));
}
#[cfg(unix)]
#[test]
fn pe_has_authenticode_table_rejects_non_pe() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let path = std::env::temp_dir().join(format!(
"enclaveapp-pe-notpe-{}-{}.exe",
std::process::id(),
SCRIPT_COUNTER.fetch_add(1, Ordering::SeqCst)
));
let content: Vec<u8> = b"#!/bin/sh\n# not a PE image\nexit 0\n"
.iter()
.copied()
.chain(std::iter::repeat(b'x').take(128))
.collect();
fs::write(&path, &content).unwrap();
let err = pe_has_authenticode_table(&path).unwrap_err();
assert!(err.contains("MZ"), "expected MZ-missing error, got: {err}");
drop(fs::remove_file(&path));
}
#[cfg(unix)]
#[test]
fn check_bridge_signature_skips_non_exe_paths_when_required() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script("unsigned-script.sh", "#!/bin/sh\nexit 0\n");
check_bridge_signature(&script, true).unwrap();
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn check_bridge_signature_rejects_unsigned_exe_when_required() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let bytes = make_pe_bytes(0, 0x10b);
let path = std::env::temp_dir().join(format!(
"enclaveapp-pe-rejected-{}-{}.exe",
std::process::id(),
SCRIPT_COUNTER.fetch_add(1, Ordering::SeqCst)
));
fs::write(&path, &bytes).unwrap();
let err = check_bridge_signature(&path, true).unwrap_err();
assert!(
err.to_string().contains("no Authenticode signature"),
"got: {err}"
);
drop(fs::remove_file(&path));
}
#[cfg(unix)]
#[test]
fn check_bridge_signature_accepts_unsigned_exe_when_not_required() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let bytes = make_pe_bytes(0, 0x10b);
let path = std::env::temp_dir().join(format!(
"enclaveapp-pe-default-{}-{}.exe",
std::process::id(),
SCRIPT_COUNTER.fetch_add(1, Ordering::SeqCst)
));
fs::write(&path, &bytes).unwrap();
check_bridge_signature(&path, false).unwrap();
drop(fs::remove_file(&path));
}
#[cfg(unix)]
#[test]
fn find_bridge_env_var_override_wins_when_path_exists() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script("override-bridge", "#!/bin/sh\nexit 0\n");
std::env::set_var("ENCLAVEAPP_BRIDGE_PATH", &script);
let found = find_bridge("some-app-that-has-no-admin-install");
std::env::remove_var("ENCLAVEAPP_BRIDGE_PATH");
assert_eq!(found.as_deref(), Some(script.as_path()));
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn find_bridge_env_var_override_ignored_when_path_missing() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
std::env::set_var("ENCLAVEAPP_BRIDGE_PATH", "/nonexistent/path/to/bridge.exe");
let found = find_bridge("another-nonexistent-app");
std::env::remove_var("ENCLAVEAPP_BRIDGE_PATH");
assert!(found.is_none());
}
#[cfg(unix)]
#[test]
fn bridge_init_sends_key_label() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"init.sh",
r#"#!/bin/sh
read request_line
case "$request_line" in
*'"method":"init"'*'"app_name":"awsenc"'*'"key_label":"cache-key"'*) printf '{"result":"","error":null}\n' ;;
*) printf '{"result":null,"error":"unexpected request"}\n' ;;
esac
"#,
);
bridge_init(&script, "awsenc", "cache-key", AccessPolicy::BiometricOnly).unwrap();
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_encrypt_initializes_before_encrypting() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"encrypt.sh",
r#"#!/bin/sh
read init_line
case "$init_line" in
*'"method":"init"'*'"key_label":"cache-key"'*) printf '{"result":"","error":null}\n' ;;
*) printf '{"result":null,"error":"unexpected init request"}\n'; exit 0 ;;
esac
read request_line
case "$request_line" in
*'"method":"encrypt"'*) printf '{"result":"aGVsbG8=","error":null}\n' ;;
*) printf '{"result":null,"error":"unexpected request"}\n' ;;
esac
"#,
);
let plaintext = bridge_encrypt(
&script,
"awsenc",
"cache-key",
b"ignored",
AccessPolicy::BiometricOnly,
)
.unwrap();
assert_eq!(plaintext, b"hello");
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_delete_sends_delete_request() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"delete.sh",
r#"#!/bin/sh
read request_line
case "$request_line" in
*'"method":"delete"'*'"key_label":"cache-key"'*) printf '{"result":"","error":null}\n' ;;
*) printf '{"result":null,"error":"unexpected request"}\n' ;;
esac
"#,
);
bridge_destroy(&script, "awsenc", "cache-key").unwrap();
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_encrypt_rejects_missing_result_payload() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"encrypt-missing-result.sh",
r#"#!/bin/sh
read init_line
printf '{"result":"","error":null}\n'
read request_line
printf '{"result":null,"error":null}\n'
"#,
);
let error = bridge_encrypt(
&script,
"awsenc",
"cache-key",
b"ignored",
AccessPolicy::None,
)
.unwrap_err();
assert!(error.to_string().contains("missing result payload"));
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_init_rejects_missing_result_payload() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"init-missing-result.sh",
r#"#!/bin/sh
read request_line
printf '{"result":null,"error":null}\n'
"#,
);
let error = bridge_init(&script, "awsenc", "cache-key", AccessPolicy::None).unwrap_err();
assert!(error.to_string().contains("missing result payload"));
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_rejects_oversized_response() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"oversized.sh",
"#!/bin/sh\nread req\npython3 -c \"print('{\\\"result\\\":\\\"' + 'A' * 70000 + '\\\",\\\"error\\\":null}')\"\n",
);
let request = BridgeRequest {
method: "init".to_string(),
params: BridgeParams::new(
String::new(),
AccessPolicy::None,
"test".into(),
"key".into(),
),
};
let err = call_bridge(&script, &request).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("byte limit") || msg.contains("bridge response"),
"expected size limit error, got: {msg}"
);
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_decrypt_initializes_before_decrypting() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"decrypt.sh",
r#"#!/bin/sh
read init_line
case "$init_line" in
*'"method":"init"'*) printf '{"result":"","error":null}\n' ;;
*) printf '{"result":null,"error":"unexpected init"}\n'; exit 0 ;;
esac
read request_line
case "$request_line" in
*'"method":"decrypt"'*) printf '{"result":"cGxhaW50ZXh0","error":null}\n' ;;
*) printf '{"result":null,"error":"unexpected request"}\n' ;;
esac
"#,
);
let result =
bridge_decrypt(&script, "test-app", "key", b"ignored", AccessPolicy::None).unwrap();
assert_eq!(result, b"plaintext");
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_decrypt_rejects_missing_result_payload() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"decrypt-missing.sh",
r#"#!/bin/sh
read init_line
printf '{"result":"","error":null}\n'
read request_line
printf '{"result":null,"error":null}\n'
"#,
);
let err =
bridge_decrypt(&script, "test-app", "key", b"ignored", AccessPolicy::None).unwrap_err();
assert!(err.to_string().contains("missing result payload"));
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_destroy_sends_delete_method_on_wire() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"wire-method.sh",
r#"#!/bin/sh
read request_line
case "$request_line" in
*'"method":"delete"'*) printf '{"result":"","error":null}\n' ;;
*'"method":"destroy"'*) printf '{"result":null,"error":"got destroy instead of delete"}\n' ;;
*) printf '{"result":null,"error":"unexpected method"}\n' ;;
esac
"#,
);
bridge_destroy(&script, "test-app", "key").unwrap();
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_delete_alias_works() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"delete-alias.sh",
r#"#!/bin/sh
read req
printf '{"result":"","error":null}\n'
"#,
);
bridge_delete(&script, "test-app", "key").unwrap();
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_session_drop_kills_child() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script("drop-kill.sh", "#!/bin/sh\nwhile true; do sleep 1; done\n");
let child_pid: u32;
{
let session = BridgeSession::spawn(&script).unwrap();
child_pid = session.child.id();
}
std::thread::sleep(Duration::from_millis(100));
let status = Command::new("kill")
.args(["-0", &child_pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
assert!(
status.is_err() || !status.unwrap().success(),
"bridge child (pid={child_pid}) should no longer exist after Drop"
);
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_init_encodes_access_policy_only() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"access-policy-only.sh",
r#"#!/bin/sh
read request_line
case "$request_line" in
*'"biometric"'*) printf '{"result":null,"error":"legacy biometric field leaked onto wire"}\n' ;;
*'"access_policy":"biometric_only"'*) printf '{"result":"","error":null}\n' ;;
*) printf '{"result":null,"error":"missing access_policy"}\n' ;;
esac
"#,
);
bridge_init(&script, "test-app", "key", AccessPolicy::BiometricOnly).unwrap();
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_rejects_empty_response() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script("empty-response.sh", "#!/bin/sh\nread req\n");
let request = BridgeRequest {
method: "init".to_string(),
params: BridgeParams::new(String::new(), AccessPolicy::None, "t".into(), "k".into()),
};
let err = call_bridge(&script, &request).unwrap_err();
assert!(
err.to_string().contains("no response") || err.to_string().contains("bridge"),
"expected bridge error, got: {}",
err
);
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_rejects_invalid_json_response() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script("invalid-json.sh", "#!/bin/sh\nread req\necho 'not json'\n");
let request = BridgeRequest {
method: "init".to_string(),
params: BridgeParams::new(String::new(), AccessPolicy::None, "t".into(), "k".into()),
};
let err = call_bridge(&script, &request).unwrap_err();
assert!(err.to_string().contains("bridge response"));
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_init_signing_sends_init_signing_method() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"init-signing.sh",
r#"#!/bin/sh
read request_line
case "$request_line" in
*'"method":"init_signing"'*'"app_name":"sshenc"'*'"key_label":"default"'*) printf '{"result":"","error":null}\n' ;;
*) printf '{"result":null,"error":"unexpected request"}\n' ;;
esac
"#,
);
bridge_init_signing(&script, "sshenc", "default", AccessPolicy::None).unwrap();
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_sign_initializes_before_signing() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"sign.sh",
r#"#!/bin/sh
read init_line
case "$init_line" in
*'"method":"init_signing"'*) printf '{"result":"","error":null}\n' ;;
*) printf '{"result":null,"error":"unexpected init request"}\n'; exit 0 ;;
esac
read request_line
case "$request_line" in
*'"method":"sign"'*) printf '{"result":"c2lnbmF0dXJl","error":null}\n' ;;
*) printf '{"result":null,"error":"unexpected request"}\n' ;;
esac
"#,
);
let signature =
bridge_sign(&script, "sshenc", "default", b"data", AccessPolicy::None).unwrap();
assert_eq!(signature, b"signature");
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_public_key_is_standalone() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"pubkey.sh",
r#"#!/bin/sh
read request_line
case "$request_line" in
*'"method":"public_key"'*) printf '{"result":"cHVia2V5","error":null}\n' ;;
*) printf '{"result":null,"error":"unexpected request"}\n' ;;
esac
"#,
);
let pubkey = bridge_public_key(&script, "sshenc", "default", AccessPolicy::None).unwrap();
assert_eq!(pubkey, b"pubkey");
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_list_keys_is_standalone() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let dir = std::env::temp_dir().join("bridge-list-keys-test");
fs::create_dir_all(&dir).unwrap();
let resp = dir.join("resp.json");
fs::write(
&resp,
"{\"result\":\"[\\\"key1\\\",\\\"key2\\\"]\",\"error\":null}\n",
)
.unwrap();
let script = temp_script(
"list-keys.sh",
&format!("#!/bin/sh\nread request_line\ncat {}\n", resp.display()),
);
let keys = bridge_list_keys(&script, "sshenc", "default", AccessPolicy::None).unwrap();
assert_eq!(keys, vec!["key1".to_string(), "key2".to_string()]);
drop(fs::remove_dir_all(&dir));
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_delete_signing_sends_delete_signing_method() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"delete-signing.sh",
r#"#!/bin/sh
read request_line
case "$request_line" in
*'"method":"delete_signing"'*'"key_label":"default"'*) printf '{"result":"","error":null}\n' ;;
*) printf '{"result":null,"error":"unexpected request"}\n' ;;
esac
"#,
);
bridge_delete_signing(&script, "sshenc", "default").unwrap();
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_signing_key_exists_returns_true() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"exists-true.sh",
r#"#!/bin/sh
read request_line
case "$request_line" in
*'"method":"signing_key_exists"'*'"key_label":"mine"'*) printf '{"result":"true","error":null}\n' ;;
*) printf '{"result":null,"error":"unexpected request"}\n' ;;
esac
"#,
);
let exists = bridge_signing_key_exists(&script, "sshenc", "mine").unwrap();
assert!(exists);
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_signing_key_exists_returns_false() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script(
"exists-false.sh",
r#"#!/bin/sh
read request_line
case "$request_line" in
*'"method":"signing_key_exists"'*) printf '{"result":"false","error":null}\n' ;;
*) printf '{"result":null,"error":"unexpected request"}\n' ;;
esac
"#,
);
let exists = bridge_signing_key_exists(&script, "sshenc", "missing").unwrap();
assert!(!exists);
cleanup_script(&script);
}
#[cfg(unix)]
#[test]
fn bridge_signing_key_exists_does_not_call_init_signing() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let sentinel = std::env::temp_dir().join(format!(
"enclaveapp-bridge-exists-nolog-{}-{}",
std::process::id(),
SCRIPT_COUNTER.fetch_add(1, Ordering::SeqCst)
));
drop(fs::remove_file(&sentinel));
let script = temp_script(
"exists-no-init.sh",
&format!(
r#"#!/bin/sh
read request_line
echo "$request_line" >> "{sentinel}"
case "$request_line" in
*'"method":"init_signing"'*) printf '{{"result":null,"error":"should not init"}}\n'; exit 0 ;;
*'"method":"signing_key_exists"'*) printf '{{"result":"false","error":null}}\n' ;;
*) printf '{{"result":null,"error":"unexpected method"}}\n' ;;
esac
"#,
sentinel = sentinel.display()
),
);
let exists = bridge_signing_key_exists(&script, "sshenc", "probe").unwrap();
assert!(!exists);
let log = fs::read_to_string(&sentinel).unwrap_or_default();
assert!(
!log.contains("init_signing"),
"exists-check must not invoke init_signing, log was: {log}"
);
cleanup_script(&script);
drop(fs::remove_file(&sentinel));
}
#[cfg(unix)]
#[test]
fn bridge_read_times_out_on_silent_bridge() {
let _lock = SCRIPT_TEST_MUTEX.lock().unwrap();
let script = temp_script("silent-bridge.sh", "#!/bin/sh\nread req\nsleep 120\n");
std::env::set_var("ENCLAVEAPP_BRIDGE_TIMEOUT_SECS", "1");
let start = std::time::Instant::now();
let request = BridgeRequest {
method: "init".to_string(),
params: BridgeParams::new(String::new(), AccessPolicy::None, "t".into(), "k".into()),
};
let err = call_bridge(&script, &request).unwrap_err();
std::env::remove_var("ENCLAVEAPP_BRIDGE_TIMEOUT_SECS");
assert!(
start.elapsed() < Duration::from_secs(10),
"timeout should fire quickly, took {:?}",
start.elapsed()
);
let msg = err.to_string();
assert!(
msg.contains("did not respond") || msg.contains("timeout"),
"expected timeout error, got: {msg}"
);
cleanup_script(&script);
}
}