use {
crate::{
code_resources::{CodeResourcesBuilder, CodeResourcesRule},
error::AppleCodesignError,
macho::{AppleSignable, CodeSigningSlot, RequirementType},
macho_signing::MachOSigner,
signing::{SettingsScope, SigningSettings},
},
goblin::mach::Mach,
slog::{info, warn, Logger},
std::{
collections::BTreeMap,
io::Write,
path::{Path, PathBuf},
},
tugger_apple_bundle::{DirectoryBundle, DirectoryBundleFile},
};
pub struct BundleSigner {
bundles: BTreeMap<Option<String>, SingleBundleSigner>,
}
impl BundleSigner {
pub fn new_from_path(path: impl AsRef<Path>) -> Result<Self, AppleCodesignError> {
let main_bundle = DirectoryBundle::new_from_path(path.as_ref())
.map_err(AppleCodesignError::DirectoryBundle)?;
let mut bundles = main_bundle
.nested_bundles()
.map_err(AppleCodesignError::DirectoryBundle)?
.into_iter()
.map(|(k, bundle)| (Some(k), SingleBundleSigner::new(bundle)))
.collect::<BTreeMap<Option<String>, SingleBundleSigner>>();
bundles.insert(None, SingleBundleSigner::new(main_bundle));
Ok(Self { bundles })
}
pub fn write_signed_bundle(
&self,
log: &Logger,
dest_dir: impl AsRef<Path>,
settings: &SigningSettings,
) -> Result<DirectoryBundle, AppleCodesignError> {
let dest_dir = dest_dir.as_ref();
let mut additional_files = Vec::new();
for (rel, nested) in &self.bundles {
match rel {
Some(rel) => {
let nested_dest_dir = dest_dir.join(rel);
info!(
log,
"entering nested bundle {}",
nested.bundle.root_dir().display(),
);
let signed_bundle = nested.write_signed_bundle(
log,
nested_dest_dir,
&settings.as_nested_bundle_settings(rel),
&[],
)?;
let main_exe = signed_bundle
.files(false)
.map_err(AppleCodesignError::DirectoryBundle)?
.into_iter()
.find(|file| matches!(file.is_main_executable(), Ok(true)));
if let Some(main_exe) = main_exe {
let macho_data = std::fs::read(main_exe.absolute_path())?;
let macho_info = SignedMachOInfo::parse_data(&macho_data)?;
let path = rel.replace('\\', "/");
let path = path.strip_prefix("Contents/").unwrap_or(&path).to_string();
additional_files.push((path, macho_info));
}
info!(
log,
"leaving nested bundle {}",
nested.bundle.root_dir().display()
);
}
None => {}
}
}
let main = self
.bundles
.get(&None)
.expect("main bundle should have a key");
main.write_signed_bundle(log, dest_dir, settings, &additional_files)
}
}
pub struct SignedMachOInfo {
pub code_directory_blob: Vec<u8>,
pub designated_code_requirement: Option<String>,
}
impl SignedMachOInfo {
pub fn parse_data(data: &[u8]) -> Result<Self, AppleCodesignError> {
let macho = match Mach::parse(data)? {
Mach::Binary(macho) => macho,
Mach::Fat(multi_arch) => multi_arch.get(0)?,
};
let signature = macho
.code_signature()?
.ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
let cd = signature
.find_slot(CodeSigningSlot::CodeDirectory)
.ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
let code_directory_blob = cd.data.to_vec();
let designated_code_requirement = if let Some(requirements) =
signature.code_requirements()?
{
if let Some(designated) = requirements.requirements.get(&RequirementType::Designated) {
let req = designated.parse_expressions()?;
Some(format!("{}", req[0]))
} else {
None
}
} else {
None
};
Ok(SignedMachOInfo {
code_directory_blob,
designated_code_requirement,
})
}
}
pub trait BundleFileHandler {
fn install_file(
&self,
log: &Logger,
file: &DirectoryBundleFile,
) -> Result<(), AppleCodesignError>;
fn sign_and_install_macho(
&self,
log: &Logger,
file: &DirectoryBundleFile,
) -> Result<SignedMachOInfo, AppleCodesignError>;
}
struct SingleBundleHandler<'a, 'key> {
settings: &'a SigningSettings<'key>,
dest_dir: PathBuf,
}
impl<'a, 'key> BundleFileHandler for SingleBundleHandler<'a, 'key> {
fn install_file(
&self,
log: &Logger,
file: &DirectoryBundleFile,
) -> Result<(), AppleCodesignError> {
let source_path = file.absolute_path();
let dest_path = self.dest_dir.join(file.relative_path());
if source_path != dest_path {
info!(
log,
"copying file {} -> {}",
source_path.display(),
dest_path.display()
);
std::fs::create_dir_all(
dest_path
.parent()
.expect("parent directory should be available"),
)?;
std::fs::copy(source_path, dest_path)?;
}
Ok(())
}
fn sign_and_install_macho(
&self,
log: &Logger,
file: &DirectoryBundleFile,
) -> Result<SignedMachOInfo, AppleCodesignError> {
info!(
log,
"signing Mach-O file {}",
file.relative_path().display()
);
let macho_data = std::fs::read(file.absolute_path())?;
let signer = MachOSigner::new(&macho_data)?;
let mut settings = self
.settings
.as_bundle_macho_settings(file.relative_path().to_string_lossy().as_ref());
let identifier = file
.relative_path()
.file_name()
.expect("failure to extract filename (this should never happen)")
.to_string_lossy();
let identifier = identifier
.strip_suffix(".dylib")
.unwrap_or_else(|| identifier.as_ref());
settings.set_binary_identifier(SettingsScope::Main, identifier);
let mut new_data = Vec::<u8>::with_capacity(macho_data.len() + 2_usize.pow(17));
signer.write_signed_binary(&settings, &mut new_data)?;
let dest_path = self.dest_dir.join(file.relative_path());
let permissions = std::fs::metadata(file.absolute_path())?.permissions();
std::fs::create_dir_all(
dest_path
.parent()
.expect("parent directory should be available"),
)?;
{
let mut fh = std::fs::File::create(&dest_path)?;
fh.write_all(&new_data)?;
}
std::fs::set_permissions(&dest_path, permissions)?;
SignedMachOInfo::parse_data(&new_data)
}
}
pub struct SingleBundleSigner {
bundle: DirectoryBundle,
}
impl SingleBundleSigner {
pub fn new(bundle: DirectoryBundle) -> Self {
Self { bundle }
}
pub fn write_signed_bundle(
&self,
log: &Logger,
dest_dir: impl AsRef<Path>,
settings: &SigningSettings,
additional_macho_files: &[(String, SignedMachOInfo)],
) -> Result<DirectoryBundle, AppleCodesignError> {
let dest_dir = dest_dir.as_ref();
warn!(
log,
"signing bundle at {} into {}",
self.bundle.root_dir().display(),
dest_dir.display()
);
let dest_dir_root = dest_dir.to_path_buf();
let dest_dir = if self.bundle.shallow() {
dest_dir_root.clone()
} else {
dest_dir.join("Contents")
};
self.bundle
.identifier()
.map_err(AppleCodesignError::DirectoryBundle)?
.ok_or_else(|| AppleCodesignError::BundleNoIdentifier(self.bundle.info_plist_path()))?;
warn!(&log, "collecting code resources files");
let mut resources_builder = CodeResourcesBuilder::default_resources_rules()?;
resources_builder.add_exclusion_rule(CodeResourcesRule::new("^_CodeSignature/")?.exclude());
resources_builder.add_exclusion_rule(CodeResourcesRule::new("^CodeResources$")?.exclude());
let handler = SingleBundleHandler {
dest_dir: dest_dir_root.clone(),
settings: &settings,
};
let mut main_exe = None;
let mut info_plist_data = None;
for file in self
.bundle
.files(false)
.map_err(AppleCodesignError::DirectoryBundle)?
{
if file
.is_main_executable()
.map_err(AppleCodesignError::DirectoryBundle)?
{
main_exe = Some(file);
} else if file.is_info_plist() {
handler.install_file(log, &file)?;
info_plist_data = Some(std::fs::read(file.absolute_path())?);
} else {
resources_builder.process_file(log, &file, &handler)?;
}
}
for (path, info) in additional_macho_files {
resources_builder.add_signed_macho_file(path, info)?;
}
let code_resources_path = dest_dir.join("_CodeSignature").join("CodeResources");
warn!(
&log,
"writing sealed resources to {}",
code_resources_path.display()
);
std::fs::create_dir_all(code_resources_path.parent().unwrap())?;
let mut resources_data = Vec::<u8>::new();
resources_builder.write_code_resources(&mut resources_data)?;
{
let mut fh = std::fs::File::create(&code_resources_path)?;
fh.write_all(&resources_data)?;
}
if let Some(exe) = main_exe {
warn!(
log,
"signing main executable {}",
exe.relative_path().display()
);
let macho_data = std::fs::read(exe.absolute_path())?;
let signer = MachOSigner::new(&macho_data)?;
let mut settings = settings.clone();
if let Some(ident) = self
.bundle
.identifier()
.map_err(AppleCodesignError::DirectoryBundle)?
{
settings.set_binary_identifier(SettingsScope::Main, ident);
}
settings.set_code_resources_data(SettingsScope::Main, resources_data);
if let Some(info_plist_data) = info_plist_data {
settings.set_info_plist_data(SettingsScope::Main, info_plist_data);
}
let mut new_data = Vec::<u8>::with_capacity(macho_data.len() + 2_usize.pow(17));
signer.write_signed_binary(&settings, &mut new_data)?;
let dest_path = dest_dir_root.join(exe.relative_path());
let permissions = std::fs::metadata(exe.absolute_path())?.permissions();
std::fs::create_dir_all(
dest_path
.parent()
.expect("parent directory should be available"),
)?;
{
let mut fh = std::fs::File::create(&dest_path)?;
fh.write_all(&new_data)?;
}
std::fs::set_permissions(&dest_path, permissions)?;
}
DirectoryBundle::new_from_path(&dest_dir_root).map_err(AppleCodesignError::DirectoryBundle)
}
}