use anyhow::{Context, Result};
#[cfg(not(target_os = "macos"))]
use arwen_codesign::{AdhocSignOptions, adhoc_sign};
#[cfg(not(target_os = "macos"))]
use fat_macho::{Error as FatMachoError, FatReader, FatWriter};
use std::path::Path;
#[cfg(target_os = "macos")]
use std::process::Command;
#[cfg(not(target_os = "macos"))]
use tempfile::NamedTempFile;
#[cfg(not(target_os = "macos"))]
fn ad_hoc_sign_macho_bytes(data: Vec<u8>, identifier: &str) -> Result<Vec<u8>> {
match FatReader::new(&data) {
Ok(reader) => {
let mut writer = FatWriter::new();
for arch in reader.iter_arches() {
let arch = arch.with_context(|| {
format!("Failed to iterate fat Mach-O slices for {identifier}")
})?;
let signed = sign_thin_macho_slice(arch.slice(&data).to_vec(), identifier)?;
writer.add(signed).with_context(|| {
format!("Failed to rebuild fat Mach-O slices for {identifier}")
})?;
}
let mut rebuilt = Vec::new();
writer
.write_to(&mut rebuilt)
.with_context(|| format!("Failed to write fat Mach-O for {identifier}"))?;
Ok(rebuilt)
}
Err(FatMachoError::NotFatBinary) => sign_thin_macho_slice(data, identifier),
Err(err) => {
Err(err).with_context(|| format!("Failed to parse fat Mach-O for {identifier}"))
}
}
}
#[cfg(not(target_os = "macos"))]
fn sign_thin_macho_slice(data: Vec<u8>, identifier: &str) -> Result<Vec<u8>> {
adhoc_sign(data, &AdhocSignOptions::new(identifier))
.with_context(|| format!("Failed to ad-hoc codesign Mach-O slice {identifier}"))
}
#[cfg(target_os = "macos")]
pub(crate) fn ad_hoc_sign(path: &Path) -> Result<()> {
let output = Command::new("codesign")
.args(["-s", "-", "-f"])
.arg(path)
.output()
.context("Failed to run codesign command")?;
if !output.status.success() {
anyhow::bail!(
"codesign failed for {}: {}",
path.display(),
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn ad_hoc_sign(path: &Path) -> Result<()> {
let data = fs_err::read(path)?;
let identifier = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("unknown");
let signed = ad_hoc_sign_macho_bytes(data, identifier)?;
let metadata = fs_err::metadata(path)?;
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let mut temp = NamedTempFile::new_in(parent)?;
use std::io::Write;
temp.write_all(&signed)?;
temp.as_file().sync_all()?;
fs_err::set_permissions(temp.path(), metadata.permissions())?;
temp.persist(path)
.map_err(|err| err.error)
.with_context(|| format!("Failed to persist signed Mach-O {}", path.display()))?;
Ok(())
}
#[cfg(target_os = "macos")]
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
fn is_fat_macho(data: &[u8]) -> bool {
matches!(
data.get(..4),
Some([0xca, 0xfe, 0xba, 0xbe] | [0xbe, 0xba, 0xfe, 0xca])
)
}
fn compile_thin_macho(dir: &Path, arch: &str) -> std::path::PathBuf {
const MINIMAL_C_SOURCE: &str = "int main(){return 0;}";
let src = dir.join("main.c");
let out = dir.join(format!("main_{arch}"));
fs_err::write(&src, MINIMAL_C_SOURCE).unwrap();
let status = Command::new("clang")
.args([
"-arch",
arch,
"-Wl,-adhoc_codesign",
"-o",
])
.arg(&out)
.arg(&src)
.status()
.expect("Failed to run clang");
assert!(status.success(), "clang failed for {arch}");
out
}
#[test]
fn signs_thin_binary_and_verifies() {
let temp_dir = tempfile::tempdir().unwrap();
let thin = compile_thin_macho(temp_dir.path(), "arm64");
ad_hoc_sign(&thin).unwrap();
let output = Command::new("codesign")
.args(["--verify", "--verbose"])
.arg(&thin)
.output()
.unwrap();
assert!(
output.status.success(),
"codesign --verify failed for thin binary: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn signs_thin_x86_64_binary_and_verifies() {
let temp_dir = tempfile::tempdir().unwrap();
let thin = compile_thin_macho(temp_dir.path(), "x86_64");
ad_hoc_sign(&thin).unwrap();
let output = Command::new("codesign")
.args(["--verify", "--verbose"])
.arg(&thin)
.output()
.unwrap();
assert!(
output.status.success(),
"codesign --verify failed for thin x86_64 binary: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn signs_fat_binary_from_thin_slices() {
use fat_macho::{FatReader, FatWriter};
let temp_dir = tempfile::tempdir().unwrap();
let arm64 = compile_thin_macho(temp_dir.path(), "arm64");
let x86_64 = compile_thin_macho(temp_dir.path(), "x86_64");
let arm64_data = fs_err::read(&arm64).unwrap();
let x86_64_data = fs_err::read(&x86_64).unwrap();
let mut writer = FatWriter::new();
writer.add(arm64_data).unwrap();
writer.add(x86_64_data).unwrap();
let mut fat = Vec::new();
writer.write_to(&mut fat).unwrap();
assert!(is_fat_macho(&fat), "Expected fat binary");
let fat_path = temp_dir.path().join("universal");
fs_err::write(&fat_path, &fat).unwrap();
ad_hoc_sign(&fat_path).unwrap();
let signed = fs_err::read(&fat_path).unwrap();
let reader = FatReader::new(&signed).unwrap();
assert!(reader.extract("arm64").is_some(), "arm64 slice missing");
assert!(reader.extract("x86_64").is_some(), "x86_64 slice missing");
let output = Command::new("codesign")
.args(["--verify", "--verbose"])
.arg(&fat_path)
.output()
.unwrap();
assert!(
output.status.success(),
"codesign --verify failed for fat binary: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}