use axoasset::LocalAsset;
use axoprocess::Cmd;
use base64::Engine;
use camino::{Utf8Path, Utf8PathBuf};
use cargo_dist_schema::TripleNameRef;
use temp_dir::TempDir;
use tracing::warn;
use crate::{create_tmp, DistError, DistResult};
struct Keychain {
_root: TempDir,
root_path: Utf8PathBuf,
password: String,
pub path: Utf8PathBuf,
}
impl Keychain {
pub fn create(password: String) -> DistResult<Self> {
let (root, root_path) = create_tmp()?;
let path = root_path.join("signing.keychain-db");
let mut cmd = Cmd::new("/usr/bin/security", "create keychain");
cmd.arg("create-keychain");
cmd.arg("-p").arg(&password);
cmd.arg(&path);
cmd.stdout_to_stderr();
cmd.status()?;
let mut cmd = Cmd::new("/usr/bin/security", "set timeout");
cmd.arg("set-keychain-settings");
cmd.arg("-lut").arg("21600");
cmd.arg(&path);
cmd.stdout_to_stderr();
cmd.status()?;
let mut cmd = Cmd::new("/usr/bin/security", "unlock keychain");
cmd.arg("unlock-keychain");
cmd.arg("-p").arg(&password);
cmd.arg(&path);
cmd.stdout_to_stderr();
cmd.status()?;
let mut cmd = Cmd::new("/usr/bin/security", "set keychain as default");
cmd.arg("default-keychain");
cmd.arg("-s");
cmd.arg(&path);
cmd.stdout_to_stderr();
cmd.status()?;
Ok(Self {
_root: root,
root_path,
password,
path,
})
}
pub fn import_certificate(&self, certificate: &[u8], passphrase: &str) -> DistResult<()> {
let cert_path = self.root_path.join("cert.p12");
LocalAsset::new(&cert_path, certificate.to_owned())?.write_to_dir(&self.root_path)?;
let mut cmd = Cmd::new("/usr/bin/security", "import certificate");
cmd.arg("import");
cmd.arg(&cert_path);
cmd.arg("-k").arg(&self.path);
cmd.arg("-P").arg(passphrase);
cmd.arg("-t").arg("cert");
cmd.arg("-f").arg("pkcs12");
cmd.arg("-A");
cmd.arg("-T")
.arg("/usr/bin/codesign")
.arg("-T")
.arg("/usr/bin/security")
.arg("-T")
.arg("/usr/bin/productsign");
cmd.stdout_to_stderr();
cmd.status()?;
let mut cmd = Cmd::new("/usr/bin/security", "configure certificate for signing");
cmd.arg("set-key-partition-list");
cmd.arg("-S").arg("apple-tool:,apple:,codesign:");
cmd.arg("-k").arg(&self.password);
cmd.arg(&self.path);
cmd.stdout_to_stderr();
cmd.status()?;
Ok(())
}
}
#[derive(Debug)]
pub struct Codesign {
env: CodesignEnv,
}
struct CodesignEnv {
pub identity: String,
pub password: String,
pub certificate: Vec<u8>,
pub options: Option<String>,
}
impl CodesignEnv {
pub fn from(
identity: &str,
password: &str,
raw_certificate: &str,
options: Option<&str>,
) -> DistResult<Self> {
let certificate = base64::prelude::BASE64_STANDARD
.decode(raw_certificate)
.map_err(|_| DistError::CertificateDecodeError {})?;
Ok(Self {
identity: identity.to_owned(),
password: password.to_owned(),
certificate,
options: options.map(ToOwned::to_owned),
})
}
}
impl std::fmt::Debug for CodesignEnv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CodesignEnv")
.field("identity", &"<hidden>")
.field("password", &"<hidden>")
.field("certificate", &"<hidden>")
.finish()
}
}
impl Codesign {
pub fn new(host_target: &TripleNameRef) -> DistResult<Option<Self>> {
if !host_target.is_darwin() {
return Ok(None);
}
if let (Some(identity), Some(password), Some(certificate), options) = (
Self::var("CODESIGN_IDENTITY", true),
Self::var("CODESIGN_CERTIFICATE_PASSWORD", true),
Self::var("CODESIGN_CERTIFICATE", true),
Self::var("CODESIGN_OPTIONS", false),
) {
let env = CodesignEnv::from(&identity, &password, &certificate, options.as_deref())?;
Ok(Some(Self { env }))
} else {
Ok(None)
}
}
fn var(var: &str, warn: bool) -> Option<String> {
let val = std::env::var(var).ok();
if warn && val.is_none() {
warn!("{var} is missing");
}
val
}
pub fn sign(&self, file: &Utf8Path) -> DistResult<()> {
let password = uuid::Uuid::new_v4().as_hyphenated().to_string();
let keychain = Keychain::create(password)?;
keychain.import_certificate(&self.env.certificate, &self.env.password)?;
let mut cmd = Cmd::new("/usr/bin/codesign", "sign macOS artifacts");
cmd.arg("--sign").arg(&self.env.identity);
if let Some(options) = &self.env.options {
cmd.arg("--options").arg(options);
}
cmd.arg("--keychain").arg(&keychain.path);
cmd.arg(file);
cmd.stdout_to_stderr();
cmd.output()?;
Ok(())
}
}