use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
#[cfg(test)]
use std::sync::{Mutex, RwLock};
use std::time::Duration;
use crate::error::{OlError, ERR_MODEL_RELAY_CA_UNTRUSTED};
pub trait TrustStore: Send + Sync {
fn install(&self, ca_pem: &Path, sha256_hex: &str) -> InstallOutcome;
fn is_trusted(&self, sha256_hex: &str) -> Result<bool, OlError>;
fn verify_leaf(&self, leaf_pem: &str, host: &str) -> Result<bool, OlError>;
fn remove(&self, sha256_hex: &str) -> Result<bool, OlError>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InstallOutcome {
Installed,
AlreadyTrusted,
NoGuiSession,
Refused(String),
}
const READ_DEADLINE: Duration = Duration::from_secs(10);
const INSTALL_DEADLINE: Duration = Duration::from_secs(120);
#[derive(Debug, Clone)]
pub(crate) enum Ran {
Exited {
success: bool,
#[cfg_attr(not(any(target_os = "macos", windows)), allow(dead_code))]
code: Option<i32>,
stdout: String,
stderr: String,
},
Missing,
#[cfg_attr(all(test, not(unix)), allow(dead_code))]
TimedOut,
#[cfg_attr(all(test, not(unix)), allow(dead_code))]
Failed(String),
}
pub(crate) trait Runner: Send + Sync {
fn run(&self, program: &str, args: &[String], deadline: Duration) -> Ran;
}
#[cfg_attr(test, allow(dead_code))]
pub(crate) struct SystemRunner;
impl Runner for SystemRunner {
fn run(&self, program: &str, args: &[String], deadline: Duration) -> Ran {
use std::process::{Command, Stdio};
let mut child = match Command::new(program)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ran::Missing,
Err(e) => return Ran::Failed(e.to_string()),
};
let stdout_reader = child.stdout.take().map(|mut out| {
std::thread::spawn(move || {
use std::io::Read;
let mut buf = String::new();
let _ = out.read_to_string(&mut buf);
buf
})
});
let stderr_reader = child.stderr.take().map(|mut err| {
std::thread::spawn(move || {
use std::io::Read;
let mut buf = String::new();
let _ = err.read_to_string(&mut buf);
buf
})
});
let deadline_at = std::time::Instant::now() + deadline;
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break Some(status),
Ok(None) if std::time::Instant::now() >= deadline_at => {
let _ = child.kill();
let _ = child.wait();
break None;
}
Ok(None) => std::thread::sleep(Duration::from_millis(10)),
Err(_) => {
let _ = child.kill();
let _ = child.wait();
break None;
}
}
};
match status {
None => Ran::TimedOut,
Some(status) => Ran::Exited {
success: status.success(),
code: status.code(),
stdout: stdout_reader
.and_then(|h| h.join().ok())
.unwrap_or_default(),
stderr: stderr_reader
.and_then(|h| h.join().ok())
.unwrap_or_default(),
},
}
}
}
fn write_leaf_tempfile(leaf_pem: &str) -> Result<tempfile::NamedTempFile, OlError> {
let tmp = tempfile::Builder::new()
.suffix(".pem")
.tempfile()
.map_err(|e| {
OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
format!("could not create a temp file for the leaf: {e}"),
)
})?;
std::fs::write(tmp.path(), leaf_pem).map_err(|e| {
OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
format!("could not write the leaf to a temp file: {e}"),
)
})?;
Ok(tmp)
}
fn already_trusted_outcome(is_trusted: Result<bool, OlError>) -> Option<InstallOutcome> {
match is_trusted {
Ok(true) => Some(InstallOutcome::AlreadyTrusted),
Ok(false) => None,
Err(e) => Some(InstallOutcome::Refused(e.message)),
}
}
fn readback_outcome(is_trusted: Result<bool, OlError>, not_found_message: &str) -> InstallOutcome {
match is_trusted {
Ok(true) => InstallOutcome::Installed,
Ok(false) => InstallOutcome::Refused(not_found_message.to_string()),
Err(e) => InstallOutcome::Refused(e.message),
}
}
pub(crate) struct MacosStore<R: Runner> {
runner: R,
keychain: PathBuf,
env: fn(&str) -> Option<OsString>,
}
fn sha256_lines(stdout: &str) -> impl Iterator<Item = &str> {
stdout
.lines()
.filter_map(|line| line.trim_start().strip_prefix("SHA-256 hash: "))
.map(str::trim)
}
impl<R: Runner> MacosStore<R> {
fn gui_session(&self) -> bool {
if (self.env)("SSH_CONNECTION").is_some() || (self.env)("SSH_TTY").is_some() {
return false;
}
matches!(
self.runner.run("launchctl", &["managername".to_string()], READ_DEADLINE),
Ran::Exited { success: true, stdout, .. } if stdout.trim() == "Aqua"
)
}
}
impl<R: Runner> TrustStore for MacosStore<R> {
fn is_trusted(&self, sha256_hex: &str) -> Result<bool, OlError> {
let args = vec![
"find-certificate".to_string(),
"-a".to_string(),
"-Z".to_string(),
self.keychain.display().to_string(),
];
match self.runner.run("security", &args, READ_DEADLINE) {
Ran::Exited {
success: true,
stdout,
..
} => Ok(sha256_lines(&stdout).any(|h| h.eq_ignore_ascii_case(sha256_hex))),
Ran::Exited { success: false, .. } => Ok(false),
Ran::Missing | Ran::TimedOut | Ran::Failed(_) => Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
"could not run `security find-certificate` against the login keychain".to_string(),
)),
}
}
fn install(&self, ca_pem: &Path, sha256_hex: &str) -> InstallOutcome {
if let Some(outcome) = already_trusted_outcome(self.is_trusted(sha256_hex)) {
return outcome;
}
if !self.gui_session() {
return InstallOutcome::NoGuiSession;
}
let args = vec![
"add-trusted-cert".to_string(),
"-r".to_string(),
"trustRoot".to_string(),
"-p".to_string(),
"ssl".to_string(),
"-p".to_string(),
"basic".to_string(),
"-k".to_string(),
self.keychain.display().to_string(),
ca_pem.display().to_string(),
];
match self.runner.run("security", &args, INSTALL_DEADLINE) {
Ran::Exited { success: true, .. } => {}
Ran::Exited { stderr, .. } => {
let first = stderr
.lines()
.find(|l| !l.trim().is_empty())
.unwrap_or_default()
.to_string();
return InstallOutcome::Refused(first);
}
Ran::TimedOut => {
return InstallOutcome::Refused(
"no answer to the trust prompt within 120 s".to_string(),
)
}
Ran::Missing => return InstallOutcome::Refused("`security` is missing".to_string()),
Ran::Failed(e) => return InstallOutcome::Refused(e),
}
readback_outcome(
self.is_trusted(sha256_hex),
"security reported success but the login keychain does not hold the certificate",
)
}
fn verify_leaf(&self, leaf_pem: &str, host: &str) -> Result<bool, OlError> {
let tmp = write_leaf_tempfile(leaf_pem)?;
let args = vec![
"verify-cert".to_string(),
"-c".to_string(),
tmp.path().display().to_string(),
"-p".to_string(),
"ssl".to_string(),
"-n".to_string(),
host.to_string(),
"-L".to_string(),
"-q".to_string(),
];
match self.runner.run("security", &args, READ_DEADLINE) {
Ran::Exited { success, .. } => Ok(success),
Ran::Missing | Ran::TimedOut | Ran::Failed(_) => Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
"could not run `security verify-cert`".to_string(),
)),
}
}
fn remove(&self, sha256_hex: &str) -> Result<bool, OlError> {
if !self.is_trusted(sha256_hex)? {
return Ok(false);
}
let args = vec![
"delete-certificate".to_string(),
"-Z".to_string(),
sha256_hex.to_uppercase(),
"-t".to_string(),
self.keychain.display().to_string(),
];
match self.runner.run("security", &args, INSTALL_DEADLINE) {
Ran::Exited { success: true, .. } => {}
Ran::Exited { stderr, .. } => {
return Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
format!("`security delete-certificate` failed: {stderr}"),
))
}
Ran::TimedOut => {
return Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
"no answer to the delete prompt within 120 s".to_string(),
))
}
Ran::Missing => {
return Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
"`security` is missing".to_string(),
))
}
Ran::Failed(e) => return Err(OlError::new(ERR_MODEL_RELAY_CA_UNTRUSTED, e)),
}
if self.is_trusted(sha256_hex)? {
Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
"security reported success but the certificate is still in the login keychain"
.to_string(),
))
} else {
Ok(true)
}
}
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
impl MacosStore<SystemRunner> {
fn for_this_machine() -> Self {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/nonexistent-home"));
Self {
runner: SystemRunner,
keychain: home
.join("Library")
.join("Keychains")
.join("login.keychain-db"),
env: |name| std::env::var_os(name),
}
}
}
pub(crate) struct WindowsStore<R: Runner> {
runner: R,
env: fn(&str) -> Option<OsString>,
}
impl<R: Runner> WindowsStore<R> {
fn thumbprints(&self, sha256_hex: &str) -> Result<Vec<String>, OlError> {
let script = format!(
"Get-ChildItem Cert:\\CurrentUser\\Root | Where-Object {{ $_.GetCertHashString([System.Security.Cryptography.HashAlgorithmName]::SHA256) -eq '{}' }} | ForEach-Object {{ $_.Thumbprint }}",
sha256_hex.to_uppercase()
);
let args = vec![
"-NoProfile".to_string(),
"-NonInteractive".to_string(),
"-Command".to_string(),
script,
];
match self.runner.run("powershell", &args, READ_DEADLINE) {
Ran::Exited {
success: true,
stdout,
..
} => Ok(stdout
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(str::to_string)
.collect()),
Ran::Exited { success: false, .. } => Ok(vec![]),
Ran::Missing | Ran::TimedOut | Ran::Failed(_) => Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
"could not run powershell to resolve the certificate thumbprint".to_string(),
)),
}
}
fn gui_session(&self) -> bool {
if (self.env)("SSH_CONNECTION").is_some() {
return false;
}
(self.env)("SESSIONNAME").is_some()
}
}
impl<R: Runner> TrustStore for WindowsStore<R> {
fn is_trusted(&self, sha256_hex: &str) -> Result<bool, OlError> {
for thumb in self.thumbprints(sha256_hex)? {
let args = vec![
"-user".to_string(),
"-verifystore".to_string(),
"Root".to_string(),
thumb,
];
if matches!(
self.runner.run("certutil", &args, READ_DEADLINE),
Ran::Exited { success: true, .. }
) {
return Ok(true);
}
}
Ok(false)
}
fn install(&self, ca_pem: &Path, sha256_hex: &str) -> InstallOutcome {
if let Some(outcome) = already_trusted_outcome(self.is_trusted(sha256_hex)) {
return outcome;
}
if !self.gui_session() {
return InstallOutcome::NoGuiSession;
}
let args = vec![
"-user".to_string(),
"-addstore".to_string(),
"Root".to_string(),
ca_pem.display().to_string(),
];
match self.runner.run("certutil", &args, INSTALL_DEADLINE) {
Ran::Exited { success: true, .. } => {}
Ran::Exited { stderr, .. } => return InstallOutcome::Refused(stderr),
Ran::TimedOut => {
return InstallOutcome::Refused(
"no answer to the trust prompt within 120 s".to_string(),
)
}
Ran::Missing => return InstallOutcome::Refused("`certutil` is missing".to_string()),
Ran::Failed(e) => return InstallOutcome::Refused(e),
}
readback_outcome(
self.is_trusted(sha256_hex),
"certutil reported success but the certificate is not in CurrentUser\\Root",
)
}
fn verify_leaf(&self, leaf_pem: &str, _host: &str) -> Result<bool, OlError> {
let tmp = write_leaf_tempfile(leaf_pem)?;
let args = vec!["-verify".to_string(), tmp.path().display().to_string()];
match self.runner.run("certutil", &args, READ_DEADLINE) {
Ran::Exited {
success, stdout, ..
} => Ok(success
&& !stdout.contains("CERT_TRUST_IS_UNTRUSTED_ROOT")
&& !stdout.contains("0x800b0109")),
Ran::Missing | Ran::TimedOut | Ran::Failed(_) => Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
"could not run `certutil -verify`".to_string(),
)),
}
}
fn remove(&self, sha256_hex: &str) -> Result<bool, OlError> {
let thumbs = self.thumbprints(sha256_hex)?;
if thumbs.is_empty() {
return Ok(false);
}
for thumb in &thumbs {
let args = vec![
"-user".to_string(),
"-delstore".to_string(),
"Root".to_string(),
thumb.clone(),
];
if let Ran::Exited {
success: false,
stderr,
..
} = self.runner.run("certutil", &args, INSTALL_DEADLINE)
{
return Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
format!("`certutil -delstore` failed: {stderr}"),
));
}
}
Ok(!self.is_trusted(sha256_hex)?)
}
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
impl WindowsStore<SystemRunner> {
fn for_this_machine() -> Self {
Self {
runner: SystemRunner,
env: |name| std::env::var_os(name),
}
}
}
pub(crate) struct NssStore<R: Runner> {
runner: R,
db: PathBuf,
}
impl<R: Runner> NssStore<R> {
fn sql(&self) -> String {
format!("sql:{}", self.db.display())
}
fn nick(sha256_hex: &str) -> String {
format!(
"OpenLatch model relay CA {}",
sha256_hex
.chars()
.take(16)
.collect::<String>()
.to_lowercase()
)
}
fn ensure_db(&self) -> Result<(), OlError> {
if self.db.join("cert9.db").exists() {
return Ok(());
}
if !self.db.exists() {
crate::fs_secure::create_dir_owner_only(&self.db).map_err(|e| {
OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
format!("could not create {}: {e}", self.db.display()),
)
})?;
}
let args = vec![
"-N".to_string(),
"-d".to_string(),
self.sql(),
"--empty-password".to_string(),
];
match self.runner.run("certutil", &args, INSTALL_DEADLINE) {
Ran::Exited { success: true, .. } => Ok(()),
Ran::Exited { stderr, .. } => Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
format!("could not initialise the NSS database: {stderr}"),
)),
Ran::Missing => Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
"NSS certutil is not installed (Debian/Ubuntu: libnss3-tools; Fedora: nss-tools)"
.to_string(),
)),
Ran::TimedOut | Ran::Failed(_) => Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
"could not initialise the NSS database".to_string(),
)),
}
}
}
impl<R: Runner> TrustStore for NssStore<R> {
fn is_trusted(&self, sha256_hex: &str) -> Result<bool, OlError> {
let args = vec![
"-L".to_string(),
"-d".to_string(),
self.sql(),
"-n".to_string(),
Self::nick(sha256_hex),
];
Ok(matches!(
self.runner.run("certutil", &args, READ_DEADLINE),
Ran::Exited { success: true, .. }
))
}
fn install(&self, ca_pem: &Path, sha256_hex: &str) -> InstallOutcome {
if let Some(outcome) = already_trusted_outcome(self.is_trusted(sha256_hex)) {
return outcome;
}
if let Err(e) = self.ensure_db() {
return InstallOutcome::Refused(e.message);
}
let args = vec![
"-A".to_string(),
"-d".to_string(),
self.sql(),
"-t".to_string(),
"C,,".to_string(),
"-n".to_string(),
Self::nick(sha256_hex),
"-i".to_string(),
ca_pem.display().to_string(),
];
match self.runner.run("certutil", &args, INSTALL_DEADLINE) {
Ran::Exited { success: true, .. } => {}
Ran::Exited { stderr, .. } => return InstallOutcome::Refused(stderr),
Ran::Missing => return InstallOutcome::Refused(
"NSS certutil is not installed (Debian/Ubuntu: libnss3-tools; Fedora: nss-tools)"
.to_string(),
),
Ran::TimedOut | Ran::Failed(_) => {
return InstallOutcome::Refused("could not run NSS certutil".to_string())
}
}
readback_outcome(
self.is_trusted(sha256_hex),
"certutil reported success but the NSS database does not hold the certificate",
)
}
fn verify_leaf(&self, leaf_pem: &str, _host: &str) -> Result<bool, OlError> {
let nick_tmp = format!(
"OpenLatch model relay probe {:016x}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0)
);
let tmp = write_leaf_tempfile(leaf_pem)?;
let add_args = vec![
"-A".to_string(),
"-d".to_string(),
self.sql(),
"-t".to_string(),
",,".to_string(),
"-n".to_string(),
nick_tmp.clone(),
"-i".to_string(),
tmp.path().display().to_string(),
];
let add_result: Result<(), OlError> =
match self.runner.run("certutil", &add_args, READ_DEADLINE) {
Ran::Exited { success: true, .. } => Ok(()),
Ran::Exited { stderr, .. } => Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
format!("could not import the probe leaf: {stderr}"),
)),
Ran::Missing => Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
"NSS certutil is not installed (Debian/Ubuntu: libnss3-tools; Fedora: \
nss-tools)"
.to_string(),
)),
Ran::TimedOut | Ran::Failed(_) => Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
"could not run NSS certutil to import the probe leaf".to_string(),
)),
};
let verify_result = add_result.map(|()| {
matches!(
self.runner.run(
"certutil",
&[
"-V".to_string(),
"-d".to_string(),
self.sql(),
"-n".to_string(),
nick_tmp.clone(),
"-u".to_string(),
"V".to_string(),
],
READ_DEADLINE,
),
Ran::Exited { success: true, .. }
)
});
let _ = self.runner.run(
"certutil",
&[
"-D".to_string(),
"-d".to_string(),
self.sql(),
"-n".to_string(),
nick_tmp,
],
READ_DEADLINE,
);
verify_result
}
fn remove(&self, sha256_hex: &str) -> Result<bool, OlError> {
if !self.is_trusted(sha256_hex)? {
return Ok(false);
}
let args = vec![
"-D".to_string(),
"-d".to_string(),
self.sql(),
"-n".to_string(),
Self::nick(sha256_hex),
];
match self.runner.run("certutil", &args, INSTALL_DEADLINE) {
Ran::Exited { success: true, .. } => Ok(!self.is_trusted(sha256_hex)?),
Ran::Exited { stderr, .. } => Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
format!("`certutil -D` failed: {stderr}"),
)),
Ran::Missing | Ran::TimedOut | Ran::Failed(_) => Err(OlError::new(
ERR_MODEL_RELAY_CA_UNTRUSTED,
"could not run NSS certutil to remove the certificate".to_string(),
)),
}
}
}
#[cfg_attr(any(target_os = "macos", target_os = "windows"), allow(dead_code))]
impl NssStore<SystemRunner> {
fn for_this_machine() -> Self {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/nonexistent-home"));
Self {
runner: SystemRunner,
db: home.join(".pki").join("nssdb"),
}
}
}
pub fn platform_proven() -> bool {
cfg!(target_os = "macos")
}
#[cfg_attr(test, allow(dead_code))]
pub(crate) fn platform() -> Arc<dyn TrustStore> {
#[cfg(target_os = "macos")]
{
Arc::new(MacosStore::<SystemRunner>::for_this_machine())
}
#[cfg(target_os = "windows")]
{
Arc::new(WindowsStore::<SystemRunner>::for_this_machine())
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
Arc::new(NssStore::<SystemRunner>::for_this_machine())
}
}
pub fn install_command(ca_pem: &Path) -> String {
let pem = crate::hooks::hook_files::quote_posix(&ca_pem.display().to_string());
#[cfg(target_os = "macos")]
{
format!(
"security add-trusted-cert -r trustRoot -p ssl -p basic -k \"$HOME/Library/Keychains/login.keychain-db\" {pem}"
)
}
#[cfg(target_os = "windows")]
{
format!("certutil -user -addstore Root {pem}")
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
format!(
"certutil -A -d \"sql:$HOME/.pki/nssdb\" -t C,, -n 'OpenLatch model relay CA' -i {pem}"
)
}
}
#[cfg_attr(test, allow(dead_code))]
static PLATFORM_SLOT: OnceLock<Arc<dyn TrustStore>> = OnceLock::new();
#[cfg(test)]
static TEST_SLOT: RwLock<Option<Arc<dyn TrustStore>>> = RwLock::new(None);
#[cfg(test)]
static TEST_SLOT_LOCK: Mutex<()> = Mutex::new(());
#[cfg(not(test))]
pub fn store() -> Arc<dyn TrustStore> {
PLATFORM_SLOT.get_or_init(platform).clone()
}
#[cfg(test)]
pub fn store() -> Arc<dyn TrustStore> {
TEST_SLOT
.read()
.unwrap_or_else(|e| e.into_inner())
.clone()
.unwrap_or_else(|| {
panic!(
"trust_store::store() reached under cfg(test) with no fake installed — call \
trust_store::install_for_tests(..) first; a test must never touch a real trust store"
)
})
}
#[cfg(test)]
pub struct TrustStoreGuard {
_lock: std::sync::MutexGuard<'static, ()>,
}
#[cfg(test)]
impl Drop for TrustStoreGuard {
fn drop(&mut self) {
*TEST_SLOT.write().unwrap_or_else(|e| e.into_inner()) = None;
}
}
#[cfg(test)]
pub fn install_for_tests(fake: Arc<dyn TrustStore>) -> TrustStoreGuard {
let lock = TEST_SLOT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
*TEST_SLOT.write().unwrap_or_else(|e| e.into_inner()) = Some(fake);
TrustStoreGuard { _lock: lock }
}
#[cfg(test)]
pub(crate) fn hold_empty_slot_for_tests() -> std::sync::MutexGuard<'static, ()> {
TEST_SLOT_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[cfg(test)]
pub mod test_support {
use super::{InstallOutcome, OlError, Path, TrustStore};
use std::collections::BTreeSet;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Mutex;
#[derive(Default)]
pub struct FakeStore {
pub installed: Mutex<BTreeSet<String>>,
pub verify: AtomicBool,
pub install_calls: AtomicUsize,
pub outcome: Mutex<Option<InstallOutcome>>,
pub calls: Mutex<Vec<(&'static str, String)>>,
pub refuse_remove: Mutex<BTreeSet<String>>,
}
impl FakeStore {
pub fn calls(&self) -> Vec<(&'static str, String)> {
self.calls.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
pub fn trust(&self, sha: &str) {
self.installed
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(sha.to_string());
}
pub fn trusted_directly(&self, sha: &str) -> bool {
self.installed
.lock()
.unwrap_or_else(|e| e.into_inner())
.contains(sha)
}
pub fn set_install_outcome(&self, o: InstallOutcome) {
*self.outcome.lock().unwrap_or_else(|e| e.into_inner()) = Some(o);
}
pub fn refuse_removal_of(&self, sha: &str) {
self.refuse_remove
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(sha.to_string());
}
pub fn allow_removal_of(&self, sha: &str) {
self.refuse_remove
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(sha);
}
}
impl TrustStore for FakeStore {
fn install(&self, _ca_pem: &Path, sha256_hex: &str) -> InstallOutcome {
self.install_calls.fetch_add(1, Ordering::SeqCst);
self.calls
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(("install", sha256_hex.to_string()));
if let Some(o) = self
.outcome
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
{
return o;
}
let mut installed = self.installed.lock().unwrap_or_else(|e| e.into_inner());
if installed.contains(sha256_hex) {
return InstallOutcome::AlreadyTrusted;
}
installed.insert(sha256_hex.to_string());
InstallOutcome::Installed
}
fn is_trusted(&self, sha256_hex: &str) -> Result<bool, OlError> {
self.calls
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(("is_trusted", sha256_hex.to_string()));
Ok(self
.installed
.lock()
.unwrap_or_else(|e| e.into_inner())
.contains(sha256_hex))
}
fn verify_leaf(&self, _leaf_pem: &str, _host: &str) -> Result<bool, OlError> {
Ok(self.verify.load(Ordering::SeqCst))
}
fn remove(&self, sha256_hex: &str) -> Result<bool, OlError> {
self.calls
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(("remove", sha256_hex.to_string()));
if self
.refuse_remove
.lock()
.unwrap_or_else(|e| e.into_inner())
.contains(sha256_hex)
{
return Err(OlError::new(
crate::error::ERR_MODEL_RELAY_CA_UNTRUSTED,
"removal refused by the store (fixture)".to_string(),
));
}
Ok(self
.installed
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(sha256_hex))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex as StdMutex;
struct ScriptedRunner {
calls: StdMutex<Vec<(String, Vec<String>)>>,
answers: StdMutex<Vec<Ran>>,
}
impl ScriptedRunner {
fn new(answers: Vec<Ran>) -> Self {
Self {
calls: StdMutex::new(Vec::new()),
answers: StdMutex::new(answers.into_iter().rev().collect()),
}
}
fn calls(&self) -> Vec<(String, Vec<String>)> {
self.calls.lock().unwrap().clone()
}
}
impl Runner for ScriptedRunner {
fn run(&self, program: &str, args: &[String], _deadline: Duration) -> Ran {
self.calls
.lock()
.unwrap()
.push((program.to_string(), args.to_vec()));
self.answers.lock().unwrap().pop().unwrap_or(Ran::Missing)
}
}
fn ok(stdout: &str) -> Ran {
Ran::Exited {
success: true,
code: Some(0),
stdout: stdout.to_string(),
stderr: String::new(),
}
}
fn fail(stderr: &str) -> Ran {
Ran::Exited {
success: false,
code: Some(1),
stdout: String::new(),
stderr: stderr.to_string(),
}
}
const SHA: &str = "AB12CD34";
#[test]
#[should_panic(expected = "install_for_tests")]
fn store_panics_without_a_fake_under_test() {
let _slot = hold_empty_slot_for_tests();
let _ = store();
}
#[test]
fn install_for_tests_serves_the_fake_and_clears_on_drop() {
let guard = install_for_tests(Arc::new(test_support::FakeStore::default()));
assert!(!store().is_trusted("ab").expect("is_trusted"));
drop(guard);
let _slot = hold_empty_slot_for_tests();
assert!(
std::panic::catch_unwind(store).is_err(),
"store() must panic once the guard is dropped"
);
}
#[test]
fn platform_proven_is_macos_only() {
assert_eq!(platform_proven(), cfg!(target_os = "macos"));
}
#[test]
fn install_command_never_names_the_system_domain() {
let cmd = install_command(Path::new("/tmp/ca.pem"));
assert!(!cmd.contains(" -d system"));
assert!(cmd.contains("/tmp/ca.pem"));
}
#[test]
#[cfg(unix)]
fn system_runner_times_out_and_kills() {
let start = std::time::Instant::now();
let ran = SystemRunner.run(
"sh",
&["-c".to_string(), "sleep 5".to_string()],
Duration::from_millis(200),
);
assert!(matches!(ran, Ran::TimedOut), "{ran:?}");
assert!(start.elapsed() < Duration::from_secs(2));
}
#[test]
fn macos_install_runs_the_exact_add_trusted_cert_argv() {
let store = MacosStore {
runner: ScriptedRunner::new(vec![
fail(""), ok("Aqua\n"), ok(""), ok("SHA-256 hash: AB12CD34\n"), ]),
keychain: PathBuf::from("/Users/dev/Library/Keychains/login.keychain-db"),
env: |_| None,
};
assert_eq!(
store.install(Path::new("/tmp/ca.pem"), SHA),
InstallOutcome::Installed
);
let calls = store.runner.calls();
assert_eq!(calls[2].0, "security");
assert_eq!(
calls[2].1,
vec![
"add-trusted-cert",
"-r",
"trustRoot",
"-p",
"ssl",
"-p",
"basic",
"-k",
"/Users/dev/Library/Keychains/login.keychain-db",
"/tmp/ca.pem",
]
);
assert!(
!calls[2].1.iter().any(|a| a == "-d"),
"never the system domain"
);
}
#[test]
fn macos_install_is_idempotent_on_sha256() {
let store = MacosStore {
runner: ScriptedRunner::new(vec![ok("SHA-256 hash: ab12cd34\n")]),
keychain: PathBuf::from("/tmp/login.keychain-db"),
env: |_| None,
};
assert_eq!(
store.install(Path::new("/tmp/ca.pem"), SHA),
InstallOutcome::AlreadyTrusted
);
assert_eq!(
store.runner.calls().len(),
1,
"add-trusted-cert must never run"
);
}
#[test]
fn macos_install_reads_back_and_refuses_an_unconfirmed_install() {
let store = MacosStore {
runner: ScriptedRunner::new(vec![fail(""), ok("Aqua\n"), ok(""), fail("")]),
keychain: PathBuf::from("/tmp/login.keychain-db"),
env: |_| None,
};
assert_eq!(
store.install(Path::new("/tmp/ca.pem"), SHA),
InstallOutcome::Refused(
"security reported success but the login keychain does not hold the certificate"
.to_string()
)
);
}
#[test]
fn macos_without_an_aqua_session_is_no_gui_session_and_runs_no_install() {
let store = MacosStore {
runner: ScriptedRunner::new(vec![fail(""), ok("Background\n")]),
keychain: PathBuf::from("/tmp/login.keychain-db"),
env: |_| None,
};
assert_eq!(
store.install(Path::new("/tmp/ca.pem"), SHA),
InstallOutcome::NoGuiSession
);
assert_eq!(store.runner.calls().len(), 2, "no add-trusted-cert call");
}
#[test]
fn macos_over_ssh_is_no_gui_session() {
let store = MacosStore {
runner: ScriptedRunner::new(vec![fail("")]),
keychain: PathBuf::from("/tmp/login.keychain-db"),
env: |name| (name == "SSH_CONNECTION").then(|| "1".into()),
};
assert_eq!(
store.install(Path::new("/tmp/ca.pem"), SHA),
InstallOutcome::NoGuiSession
);
assert_eq!(
store.runner.calls().len(),
1,
"launchctl must never even be asked over SSH"
);
}
#[test]
fn macos_a_cancelled_dialog_is_refused_with_its_reason() {
let store = MacosStore {
runner: ScriptedRunner::new(vec![fail(""), ok("Aqua\n"), fail("User canceled.\n")]),
keychain: PathBuf::from("/tmp/login.keychain-db"),
env: |_| None,
};
assert_eq!(
store.install(Path::new("/tmp/ca.pem"), SHA),
InstallOutcome::Refused("User canceled.".to_string())
);
}
#[test]
fn macos_verify_leaf_judges_the_exit_status_not_the_sct_line() {
let store = MacosStore {
runner: ScriptedRunner::new(vec![
ok("certificate verified.\n[SCT] embedded, verified ok\n"),
fail(""),
]),
keychain: PathBuf::from("/tmp/login.keychain-db"),
env: |_| None,
};
assert!(store
.verify_leaf("-----BEGIN CERTIFICATE-----", "a.test")
.expect("Ok(true)"));
let calls = store.runner.calls();
assert_eq!(calls[0].0, "security");
assert_eq!(&calls[0].1[0..2], ["verify-cert", "-c"]);
assert_eq!(&calls[0].1[3..], ["-p", "ssl", "-n", "a.test", "-L", "-q"]);
assert!(!store
.verify_leaf("-----BEGIN CERTIFICATE-----", "a.test")
.expect("Ok(false)"));
}
#[test]
fn macos_remove_deletes_by_sha256_with_trust_settings_and_reads_back() {
let store = MacosStore {
runner: ScriptedRunner::new(vec![
ok("SHA-256 hash: AB12CD34\n"), ok(""), fail(""), ]),
keychain: PathBuf::from("/tmp/login.keychain-db"),
env: |_| None,
};
assert!(store.remove(SHA).expect("remove"));
let calls = store.runner.calls();
assert_eq!(
calls[1].1,
vec![
"delete-certificate",
"-Z",
&SHA.to_uppercase(),
"-t",
"/tmp/login.keychain-db",
]
);
}
#[test]
fn macos_remove_of_an_absent_certificate_runs_no_delete() {
let store = MacosStore {
runner: ScriptedRunner::new(vec![fail("")]),
keychain: PathBuf::from("/tmp/login.keychain-db"),
env: |_| None,
};
assert!(!store.remove(SHA).expect("remove"));
assert_eq!(store.runner.calls().len(), 1, "no delete-certificate call");
}
#[test]
fn find_certificate_sha256_parsing_ignores_case_and_sha1_lines() {
let stdout = "SHA-1 hash: DEADBEEF00112233\n\
SHA-256 hash: AB12CD34EF56AB12CD34EF56AB12CD34EF56AB12CD34EF56AB12CD34EF56AB12\n\
\n\
SHA-1 hash: FEEDFACE00112233\n\
SHA-256 hash: 1122334455667788112233445566778811223344556677881122334455667788\n";
let store = MacosStore {
runner: ScriptedRunner::new(vec![ok(stdout), ok(stdout), ok(stdout)]),
keychain: PathBuf::from("/tmp/login.keychain-db"),
env: |_| None,
};
assert!(store
.is_trusted("ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12")
.expect("is_trusted, lower-case query"));
assert!(store
.is_trusted("1122334455667788112233445566778811223344556677881122334455667788")
.expect("is_trusted, second certificate"));
assert!(!store
.is_trusted("deadbeef00112233")
.expect("a SHA-1 line must never match"));
}
#[test]
fn macos_install_argv_never_carries_a_system_domain_flag() {
let store = MacosStore {
runner: ScriptedRunner::new(vec![fail(""), ok("SHA-256 hash: ab12cd34\n")]),
keychain: PathBuf::from("/tmp/login.keychain-db"),
env: |name| (name == "SESSIONNAME").then(|| "x".into()),
};
let _ = store.install(Path::new("/tmp/ca.pem"), SHA);
for (_, args) in store.runner.calls() {
assert!(!args.iter().any(|a| a == "-d" || a == "system"));
}
}
#[test]
fn windows_install_uses_the_current_user_root_store() {
let store = WindowsStore {
runner: ScriptedRunner::new(vec![
ok(""), ok(""), ok("THUMB123\n"), ok(""), ]),
env: |name| (name == "SESSIONNAME").then(|| "Console".into()),
};
assert_eq!(
store.install(Path::new("/tmp/ca.pem"), SHA),
InstallOutcome::Installed
);
let calls = store.runner.calls();
assert_eq!(calls[1].0, "certutil");
assert_eq!(
calls[1].1,
vec!["-user", "-addstore", "Root", "/tmp/ca.pem"]
);
let store2 = WindowsStore {
runner: ScriptedRunner::new(vec![ok("")]),
env: |_| None,
};
assert_eq!(
store2.install(Path::new("/tmp/ca.pem"), SHA),
InstallOutcome::NoGuiSession
);
}
#[test]
fn windows_is_trusted_maps_sha256_to_a_thumbprint_then_verifystore() {
let store = WindowsStore {
runner: ScriptedRunner::new(vec![ok("THUMB123\n"), ok("")]),
env: |_| None,
};
assert!(store.is_trusted(SHA).expect("is_trusted"));
let calls = store.runner.calls();
assert_eq!(calls[0].0, "powershell");
assert!(
calls[0].1.iter().any(|a| a.contains(&SHA.to_uppercase())),
"the script must name the upper-cased SHA: {:?}",
calls[0].1
);
assert_eq!(
calls[1].1,
vec!["-user", "-verifystore", "Root", "THUMB123"]
);
let store_empty = WindowsStore {
runner: ScriptedRunner::new(vec![ok("")]),
env: |_| None,
};
assert!(!store_empty.is_trusted(SHA).expect("no thumbprint"));
}
#[test]
fn windows_remove_uses_delstore_with_the_thumbprint() {
let store = WindowsStore {
runner: ScriptedRunner::new(vec![
ok("THUMB123\n"), ok(""), ok(""), ]),
env: |_| None,
};
assert!(store.remove(SHA).expect("remove"));
let calls = store.runner.calls();
assert_eq!(calls[1].1, vec!["-user", "-delstore", "Root", "THUMB123"]);
}
#[test]
fn nss_install_creates_the_db_when_absent_then_adds_with_ssl_ca_trust() {
let dir = tempfile::tempdir().expect("tempdir");
let db = dir.path().join("nssdb"); let store = NssStore {
runner: ScriptedRunner::new(vec![
fail(""), ok(""), ok(""), ok(""), ]),
db: db.clone(),
};
assert_eq!(
store.install(Path::new("/tmp/ca.pem"), SHA),
InstallOutcome::Installed
);
let calls = store.runner.calls();
let sql = format!("sql:{}", db.display());
assert_eq!(
calls[1].1,
vec![
"-N".to_string(),
"-d".to_string(),
sql.clone(),
"--empty-password".to_string(),
]
);
let nick = NssStore::<ScriptedRunner>::nick(SHA);
assert_eq!(
calls[2].1,
vec![
"-A".to_string(),
"-d".to_string(),
sql,
"-t".to_string(),
"C,,".to_string(),
"-n".to_string(),
nick,
"-i".to_string(),
"/tmp/ca.pem".to_string(),
]
);
assert!(db.exists(), "the directory must be created");
}
#[test]
fn nss_is_trusted_asks_by_the_sha_keyed_nickname() {
assert_eq!(
NssStore::<ScriptedRunner>::nick("ab12cd34ef56ab12cd34ef56"),
"OpenLatch model relay CA ab12cd34ef56ab12"
);
let store = NssStore {
runner: ScriptedRunner::new(vec![ok("")]),
db: PathBuf::from("/tmp/nssdb"),
};
assert!(store.is_trusted(SHA).expect("is_trusted"));
assert_eq!(
store.runner.calls()[0].1,
vec![
"-L".to_string(),
"-d".to_string(),
"sql:/tmp/nssdb".to_string(),
"-n".to_string(),
NssStore::<ScriptedRunner>::nick(SHA),
]
);
}
#[test]
fn nss_missing_certutil_is_refused_with_the_package_names() {
let dir = tempfile::tempdir().expect("tempdir");
let db = dir.path().join("nssdb");
std::fs::create_dir_all(&db).unwrap();
std::fs::write(db.join("cert9.db"), "").unwrap(); let store = NssStore {
runner: ScriptedRunner::new(vec![Ran::Missing, Ran::Missing]),
db,
};
match store.install(Path::new("/tmp/ca.pem"), SHA) {
InstallOutcome::Refused(msg) => {
assert!(msg.contains("libnss3-tools"), "{msg}");
assert!(msg.contains("nss-tools"), "{msg}");
}
other => panic!("expected Refused, got {other:?}"),
}
}
#[test]
fn nss_never_reports_no_gui_session() {
let dir = tempfile::tempdir().expect("tempdir");
let db = dir.path().join("nssdb");
std::fs::create_dir_all(&db).unwrap();
std::fs::write(db.join("cert9.db"), "").unwrap();
let store = NssStore {
runner: ScriptedRunner::new(vec![fail(""), ok(""), ok("")]),
db,
};
let outcome = store.install(Path::new("/tmp/ca.pem"), SHA);
assert_ne!(outcome, InstallOutcome::NoGuiSession);
assert_eq!(outcome, InstallOutcome::Installed);
}
#[test]
fn nss_verify_leaf_imports_verifies_and_always_deletes_a_probe_leaf() {
let store = NssStore {
runner: ScriptedRunner::new(vec![ok(""), fail(""), ok("")]),
db: PathBuf::from("/tmp/nssdb"),
};
assert!(!store
.verify_leaf("-----BEGIN CERTIFICATE-----", "a.test")
.expect("Ok(false)"));
let calls = store.runner.calls();
assert_eq!(calls.len(), 3);
assert_eq!(calls[0].1[0], "-A");
assert!(
calls[0].1.contains(&",,".to_string()),
"no trust flags on import"
);
assert_eq!(calls[1].1[0], "-V");
assert!(calls[1].1.contains(&"V".to_string()));
assert_eq!(calls[2].1[0], "-D");
let store2 = NssStore {
runner: ScriptedRunner::new(vec![fail(""), ok("")]),
db: PathBuf::from("/tmp/nssdb"),
};
let err = store2
.verify_leaf("-----BEGIN CERTIFICATE-----", "a.test")
.expect_err("an import failure must be Err, not a silent false");
assert_eq!(err.code, ERR_MODEL_RELAY_CA_UNTRUSTED);
let calls2 = store2.runner.calls();
assert_eq!(calls2.len(), 2, "-D must still run even though -A failed");
assert_eq!(calls2[0].1[0], "-A");
assert_eq!(calls2[1].1[0], "-D");
}
#[test]
fn fake_store_round_trips_install_and_remove() {
use test_support::FakeStore;
let fake = FakeStore::default();
assert_eq!(
fake.install(Path::new("/tmp/ca.pem"), SHA),
InstallOutcome::Installed
);
assert_eq!(
fake.install(Path::new("/tmp/ca.pem"), SHA),
InstallOutcome::AlreadyTrusted
);
assert!(fake.is_trusted(SHA).expect("is_trusted"));
assert!(fake.remove(SHA).expect("remove"));
assert!(!fake.is_trusted(SHA).expect("is_trusted"));
}
}