use std::path::{Path, PathBuf};
use std::process::Command;
mod setup;
pub(crate) use setup::{SetupOutcome, setup_identity};
pub(crate) const IDENTITY_CN: &str = "lean-ctx-codesign";
pub(crate) const CODESIGN_IDENTIFIER: &str = "com.leanctx.cli";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SignKind {
Stable,
AdHoc,
}
pub(crate) fn keychain_path() -> Option<PathBuf> {
Some(dirs::home_dir()?.join("Library/Keychains/lean-ctx-codesign.keychain-db"))
}
pub(crate) fn password_path() -> Option<PathBuf> {
Some(
crate::core::paths::data_dir()
.ok()?
.join("codesign-keychain.pw"),
)
}
pub(crate) fn load_password() -> Option<String> {
let pw = std::fs::read_to_string(password_path()?).ok()?;
let pw = pw.trim().to_string();
(!pw.is_empty()).then_some(pw)
}
pub(crate) fn is_ready() -> bool {
let Some(kc) = keychain_path() else {
return false;
};
if !kc.exists() {
return false;
}
let Ok(out) = Command::new("security")
.args(["find-identity", "-v", "-p", "codesigning"])
.arg(&kc)
.output()
else {
return false;
};
String::from_utf8_lossy(&out.stdout)
.lines()
.any(|line| line.contains(IDENTITY_CN) && !line.contains('('))
}
pub(crate) fn sign_binary(binary: &Path) -> SignKind {
if is_ready() && sign_with_identity(binary).is_ok() {
return SignKind::Stable;
}
adhoc_sign(binary);
SignKind::AdHoc
}
fn sign_with_identity(binary: &Path) -> Result<(), String> {
let kc = keychain_path().ok_or("no home dir")?;
if let Some(pw) = load_password() {
let _ = Command::new("security")
.args(["unlock-keychain", "-p", &pw])
.arg(&kc)
.output();
}
let out = Command::new("codesign")
.args([
"--force",
"--timestamp=none",
"--identifier",
CODESIGN_IDENTIFIER,
"--keychain",
])
.arg(&kc)
.args(["--sign", IDENTITY_CN])
.arg(binary)
.output()
.map_err(|e| format!("spawn codesign: {e}"))?;
if out.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&out.stderr).trim().to_string())
}
}
pub(crate) fn adhoc_sign(binary: &Path) {
let _ = Command::new("codesign")
.args(["--force", "-s", "-"])
.arg(binary)
.output();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keychain_path_is_dedicated_not_login() {
let kc = keychain_path().expect("home dir");
assert!(kc.ends_with("lean-ctx-codesign.keychain-db"));
assert!(!kc.to_string_lossy().contains("login.keychain"));
}
#[test]
fn password_lives_in_data_dir() {
let p = password_path().expect("data dir");
assert!(p.ends_with("codesign-keychain.pw"));
}
#[test]
fn identifier_is_stable_and_reverse_dns() {
assert_eq!(CODESIGN_IDENTIFIER, "com.leanctx.cli");
assert_eq!(IDENTITY_CN, "lean-ctx-codesign");
}
#[test]
fn sign_kind_is_comparable() {
assert_ne!(SignKind::Stable, SignKind::AdHoc);
}
}