#![allow(dead_code)]
use std::path::{Path, PathBuf};
use std::process::Command;
pub struct TempDir(PathBuf);
impl TempDir {
pub fn new(tag: &str) -> Self {
let path = std::env::temp_dir().join(format!(
"kc-test-{tag}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).expect("create scratch directory");
Self(path)
}
pub fn path(&self) -> &Path {
&self.0
}
pub fn join(&self, name: &str) -> PathBuf {
self.0.join(name)
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
pub fn security_available() -> bool {
Command::new("/usr/bin/security")
.arg("help")
.output()
.is_ok_and(|output| output.status.success() || output.status.code() == Some(1))
}
pub fn security(args: &[&str]) -> std::process::Output {
Command::new("/usr/bin/security")
.args(args)
.output()
.expect("run security")
}
pub fn security_ok(args: &[&str]) -> String {
let output = security(args);
assert!(
output.status.success(),
"security {args:?} failed: {}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
pub fn create_with_security(path: &Path, password: &str) {
let path = path.to_str().expect("utf-8 path");
security_ok(&["create-keychain", "-p", password, path]);
security_ok(&["unlock-keychain", "-p", password, path]);
}
pub fn add_generic_with_security(path: &Path, account: &str, service: &str, secret: &str) {
security_ok(&[
"add-generic-password",
"-a",
account,
"-s",
service,
"-w",
secret,
"-A",
path.to_str().expect("utf-8 path"),
]);
}
pub fn add_internet_with_security(path: &Path, account: &str, server: &str, secret: &str) {
security_ok(&[
"add-internet-password",
"-a",
account,
"-s",
server,
"-w",
secret,
"-r",
"http",
"-P",
"8080",
"-p",
"/login",
"-A",
path.to_str().expect("utf-8 path"),
]);
}
pub fn populated_keychain(dir: &TempDir, name: &str, password: &str) -> PathBuf {
let path = dir.join(name);
create_with_security(&path, password);
add_generic_with_security(&path, "alice", "myservice", "s3cr3t-generic");
add_generic_with_security(&path, "carol", "other", "third-one");
add_internet_with_security(&path, "bob", "example.com", "s3cr3t-internet");
path
}
pub fn kc_binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_kc"))
}
pub fn kc(args: &[&str], password: Option<&str>) -> std::process::Output {
use std::io::Write;
use std::process::Stdio;
let mut child = Command::new(kc_binary())
.args(args)
.stdin(if password.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn kc");
if let Some(password) = password {
child
.stdin
.take()
.expect("stdin")
.write_all(format!("{password}\n").as_bytes())
.expect("write password");
}
child.wait_with_output().expect("run kc")
}
pub fn kc_with_env(args: &[&str], env: &[(&str, &str)]) -> std::process::Output {
use std::process::Stdio;
let mut command = Command::new(kc_binary());
command.args(args).stdin(Stdio::null());
for (name, value) in env {
command.env(name, value);
}
command.output().expect("run kc")
}
pub fn kc_ok(args: &[&str], password: Option<&str>) -> String {
let output = kc(args, password);
assert!(
output.status.success(),
"kc {args:?} failed: {}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
pub fn add_generic_trusting_with_security(
path: &Path,
account: &str,
service: &str,
secret: &str,
applications: &[&str],
) {
let mut args = vec![
"add-generic-password",
"-a",
account,
"-s",
service,
"-w",
secret,
];
for application in applications {
args.push("-T");
args.push(application);
}
let path = path.to_str().expect("utf-8 path");
args.push(path);
security_ok(&args);
}
pub fn compile_requirement(expression: &str) -> Vec<u8> {
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
let dir = std::env::temp_dir().join(format!(
"kc-req-{}-{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
));
std::fs::create_dir_all(&dir).expect("scratch dir");
let out = dir.join("req.bin");
let status = Command::new("/usr/bin/csreq")
.arg(format!("-r={expression}"))
.arg("-b")
.arg(&out)
.status()
.expect("run csreq");
assert!(status.success(), "csreq failed for {expression:?}");
let blob = std::fs::read(&out).expect("read requirement");
let _ = std::fs::remove_dir_all(&dir);
blob
}
pub fn designated_requirement(binary: &str) -> Vec<u8> {
let output = Command::new("/usr/bin/codesign")
.args(["-d", "-r-", binary])
.output()
.expect("run codesign");
let text = String::from_utf8_lossy(&output.stdout);
let expression = text
.lines()
.find_map(|line| line.strip_prefix("designated => "))
.unwrap_or_else(|| panic!("no designated requirement for {binary}"));
compile_requirement(expression)
}
pub fn generate_identity(dir: &TempDir, name: &str, common_name: &str) -> (PathBuf, PathBuf) {
let certificate = dir.join(&format!("{name}-cert.pem"));
let key = dir.join(&format!("{name}-key.pem"));
let openssl_key = dir.join(&format!("{name}-key-1.pem"));
let status = Command::new("/usr/bin/openssl")
.args([
"req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "30",
])
.arg("-subj")
.arg(format!("/CN={common_name}"))
.arg("-keyout")
.arg(&openssl_key)
.arg("-out")
.arg(&certificate)
.output()
.expect("run openssl req");
assert!(
status.status.success(),
"openssl req failed: {}",
String::from_utf8_lossy(&status.stderr)
);
let status = Command::new("/usr/bin/openssl")
.args(["pkcs8", "-topk8", "-nocrypt", "-in"])
.arg(&openssl_key)
.arg("-out")
.arg(&key)
.output()
.expect("run openssl pkcs8");
assert!(
status.status.success(),
"openssl pkcs8 failed: {}",
String::from_utf8_lossy(&status.stderr)
);
(certificate, key)
}
pub fn import_identity_with_security(
dir: &TempDir,
keychain: &Path,
name: &str,
certificate: &Path,
key: &Path,
friendly_name: &str,
) {
let bundle = dir.join(&format!("{name}.p12"));
let status = Command::new("/usr/bin/openssl")
.args([
"pkcs12",
"-export",
"-macalg",
"sha1",
"-certpbe",
"PBE-SHA1-3DES",
"-keypbe",
"PBE-SHA1-3DES",
"-passout",
"pass:x",
])
.arg("-name")
.arg(friendly_name)
.arg("-inkey")
.arg(key)
.arg("-in")
.arg(certificate)
.arg("-out")
.arg(&bundle)
.output()
.expect("run openssl pkcs12");
assert!(
status.status.success(),
"openssl pkcs12 failed: {}",
String::from_utf8_lossy(&status.stderr)
);
security_ok(&[
"import",
bundle.to_str().expect("utf-8 path"),
"-k",
keychain.to_str().expect("utf-8 path"),
"-P",
"x",
"-A",
]);
}