use {
crate::{
bundle_signing::BundleSigner,
dmg::DmgSigner,
error::AppleCodesignError,
macho_signing::{write_macho_file, MachOSigner},
reader::PathType,
signing_settings::{SettingsScope, SigningSettings},
},
apple_xar::{reader::XarReader, signing::XarSigner},
log::{info, warn},
std::{fs::File, path::Path},
};
pub struct UnifiedSigner<'key> {
settings: SigningSettings<'key>,
}
impl<'key> UnifiedSigner<'key> {
pub fn new(settings: SigningSettings<'key>) -> Self {
Self { settings }
}
pub fn sign_path(
&self,
input_path: impl AsRef<Path>,
output_path: impl AsRef<Path>,
) -> Result<(), AppleCodesignError> {
let input_path = input_path.as_ref();
match PathType::from_path(input_path)? {
PathType::Bundle => self.sign_bundle(input_path, output_path),
PathType::Dmg => self.sign_dmg(input_path, output_path),
PathType::MachO => self.sign_macho(input_path, output_path),
PathType::Xar => self.sign_xar(input_path, output_path),
PathType::Zip | PathType::Other => Err(AppleCodesignError::UnrecognizedPathType),
}
}
pub fn sign_path_in_place(&self, path: impl AsRef<Path>) -> Result<(), AppleCodesignError> {
let path = path.as_ref();
self.sign_path(path, path)
}
pub fn sign_macho(
&self,
input_path: impl AsRef<Path>,
output_path: impl AsRef<Path>,
) -> Result<(), AppleCodesignError> {
let input_path = input_path.as_ref();
let output_path = output_path.as_ref();
warn!("signing {} as a Mach-O binary", input_path.display());
let macho_data = std::fs::read(input_path)?;
let mut settings = self.settings.clone();
settings.import_settings_from_macho(&macho_data)?;
if settings.binary_identifier(SettingsScope::Main).is_none() {
let identifier = path_identifier(input_path)?;
warn!("setting binary identifier to {}", identifier);
settings.set_binary_identifier(SettingsScope::Main, identifier);
}
warn!("parsing Mach-O");
let signer = MachOSigner::new(&macho_data)?;
let mut macho_data = vec![];
signer.write_signed_binary(&settings, &mut macho_data)?;
warn!("writing Mach-O to {}", output_path.display());
write_macho_file(input_path, output_path, &macho_data)?;
Ok(())
}
pub fn sign_dmg(
&self,
input_path: impl AsRef<Path>,
output_path: impl AsRef<Path>,
) -> Result<(), AppleCodesignError> {
let input_path = input_path.as_ref();
let output_path = output_path.as_ref();
warn!("signing {} as a DMG", input_path.display());
let mut settings = self.settings.clone();
if settings.binary_identifier(SettingsScope::Main).is_none() {
let file_name = input_path
.file_stem()
.ok_or_else(|| {
AppleCodesignError::CliGeneralError("unable to resolve file name of DMG".into())
})?
.to_string_lossy();
warn!(
"setting binary identifier to {} (derived from file name)",
file_name
);
settings.set_binary_identifier(SettingsScope::Main, file_name);
}
if input_path != output_path {
info!(
"copying {} to {} in preparation for signing",
input_path.display(),
output_path.display()
);
if let Some(parent) = output_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(input_path, output_path)?;
}
let signer = DmgSigner::default();
let mut fh = std::fs::File::options()
.read(true)
.write(true)
.open(output_path)?;
signer.sign_file(&settings, &mut fh)?;
Ok(())
}
pub fn sign_bundle(
&self,
input_path: impl AsRef<Path>,
output_path: impl AsRef<Path>,
) -> Result<(), AppleCodesignError> {
let input_path = input_path.as_ref();
warn!("signing bundle at {}", input_path.display());
let mut signer = BundleSigner::new_from_path(input_path)?;
signer.collect_nested_bundles()?;
signer.write_signed_bundle(output_path, &self.settings)?;
Ok(())
}
pub fn sign_xar(
&self,
input_path: impl AsRef<Path>,
output_path: impl AsRef<Path>,
) -> Result<(), AppleCodesignError> {
let input_path = input_path.as_ref();
let output_path = output_path.as_ref();
let output_path_temp =
output_path.with_file_name(if let Some(file_name) = output_path.file_name() {
file_name.to_string_lossy().to_string() + ".tmp"
} else {
"xar.tmp".to_string()
});
warn!(
"signing XAR pkg installer at {} to {}",
input_path.display(),
output_path_temp.display()
);
let (signing_key, signing_cert) = self
.settings
.signing_key()
.ok_or(AppleCodesignError::XarNoAdhoc)?;
{
let reader = XarReader::new(File::open(input_path)?)?;
let mut signer = XarSigner::new(reader);
let mut fh = File::create(&output_path_temp)?;
signer.sign(
&mut fh,
signing_key,
signing_cert,
self.settings.time_stamp_url(),
self.settings.certificate_chain().iter().cloned(),
)?;
}
if output_path.exists() {
warn!("removing existing {}", output_path.display());
std::fs::remove_file(output_path)?;
}
warn!(
"renaming {} -> {}",
output_path_temp.display(),
output_path.display()
);
std::fs::rename(&output_path_temp, output_path)?;
Ok(())
}
}
pub fn path_identifier(path: impl AsRef<Path>) -> Result<String, AppleCodesignError> {
let path = path.as_ref();
let file_name = path
.file_name()
.ok_or_else(|| {
AppleCodesignError::PathIdentifier(format!("path {} lacks a file name", path.display()))
})?
.to_string_lossy()
.to_string();
let id = if let Some((prefix, extension)) = file_name.rsplit_once('.') {
if extension.chars().all(|c| c.is_ascii_digit()) {
file_name.as_str()
} else {
prefix
}
} else {
file_name.as_str()
};
let is_digit_or_dot = |c: char| c == '.' || c.is_ascii_digit();
let id = match id.chars().next() {
Some(first) => {
if is_digit_or_dot(first) {
return Ok(id.to_string());
} else {
id
}
}
None => {
return Ok(id.to_string());
}
};
let prefix = id.trim_end_matches(is_digit_or_dot);
let stripped = &id[prefix.len()..];
if stripped.is_empty() {
Ok(id.to_string())
} else {
let (prefix, stripped) = if matches!(stripped.chars().next(), Some('.')) {
(&id[0..prefix.len() + 1], &stripped[1..])
} else {
(prefix, stripped)
};
let id = prefix
.chars()
.chain(stripped.chars().take_while(|c| c.is_ascii_digit()))
.collect::<String>();
Ok(id)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn path_identifier_normalization() {
assert_eq!(path_identifier("foo").unwrap(), "foo");
assert_eq!(path_identifier("foo.dylib").unwrap(), "foo");
assert_eq!(path_identifier("/etc/foo.dylib").unwrap(), "foo");
assert_eq!(path_identifier("/etc/foo").unwrap(), "foo");
assert_eq!(path_identifier(".foo").unwrap(), "");
assert_eq!(path_identifier("123").unwrap(), "123");
assert_eq!(path_identifier(".foo.dylib").unwrap(), ".foo");
assert_eq!(path_identifier("123.dylib").unwrap(), "123");
assert_eq!(path_identifier("123.42").unwrap(), "123.42");
assert_eq!(path_identifier("foo1").unwrap(), "foo1");
assert_eq!(path_identifier("foo1.dylib").unwrap(), "foo1");
assert_eq!(path_identifier("foo1.2.dylib").unwrap(), "foo1");
assert_eq!(path_identifier("foo1.2").unwrap(), "foo1");
assert_eq!(path_identifier("foo1.2.3.4.dylib").unwrap(), "foo1");
assert_eq!(path_identifier("foo.1").unwrap(), "foo.1");
assert_eq!(path_identifier("foo.1.2.3").unwrap(), "foo.1");
assert_eq!(path_identifier("foo.1.2.dylib").unwrap(), "foo.1");
assert_eq!(path_identifier("foo.1.dylib").unwrap(), "foo.1");
}
}