Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
a74924e
Branches
Tags
main
apple-crates
apple-codesign-quick
src
macho.rs
Refactor and unify a lot of things for another project and implement airdrop support
Dadoum
commited
a74924e
at 2026-10-25 22:15:58
macho.rs
Blame
History
Raw
use crate::error::{CodeSignError, Result}; use crate::file_bytes::read_file_bytes; use crate::signature::{ CSMAGIC_EMBEDDED_ENTITLEMENTS, CSSLOT_ENTITLEMENTS, CmsSigner, CodeDirectoryParams, ExecutableSegment, SignatureBlobs, encode_embedded_signature, entitlement_exec_flags, parse_superblob, }; use byteorder::{BigEndian, ByteOrder}; use plist::Dictionary; use rayon::prelude::*; use std::borrow::Cow; use std::fs; use std::path::Path; use thin::{MachOHeader, ThinMachO}; mod fat; mod thin; pub const DEFAULT_CMS_BLOB_RESERVATION: usize = 16 * 1024; // Our CMS blob keeps its size between attempts, but a custom CmsSigner may not. const MAX_SIGNING_ATTEMPTS: usize = 8; #[derive(Clone)] pub struct MachOSigningConfig<'a> { pub identifier: &'a str, pub team_id: &'a str, pub entitlements: &'a Dictionary, pub info_plist: Option<&'a [u8]>, pub code_resources: Option<&'a [u8]>, pub cms_signer: Option<&'a dyn CmsSigner>, pub cms_blob_reservation: usize, } impl<'a> MachOSigningConfig<'a> { pub fn new( identifier: &'a str, team_id: &'a str, entitlements: &'a Dictionary, cms_signer: Option<&'a dyn CmsSigner>, ) -> Self { MachOSigningConfig { identifier, team_id, entitlements, info_plist: None, code_resources: None, cms_signer, cms_blob_reservation: DEFAULT_CMS_BLOB_RESERVATION, } } } pub fn sign_macho_file(path: &Path, config: &MachOSigningConfig<'_>) -> Result<()> { // The file stays mapped only until it's signed in memory, it can't be while we rewrite it. let signed = { let data = read_file_bytes(path)?; sign_macho(path, Cow::Borrowed(data.as_slice()), config)? }; fs::write(path, signed).map_err(CodeSignError::io(path)) } /// Sign a Mach-O in memory; `path` only names it in errors. pub fn sign_macho_data( path: &Path, data: &[u8], config: &MachOSigningConfig<'_>, ) -> Result<Vec<u8>> { sign_macho(path, Cow::Borrowed(data), config) } /// Read the XML entitlements of a Mach-O's code signature: its arm64 slice's, else the first ones found. pub fn read_macho_entitlements(path: &Path) -> Result<Option<Dictionary>> { let data = read_file_bytes(path)?; parse_macho_entitlements(path, data.as_slice()) } /// Like `read_macho_entitlements`, for a Mach-O in memory; `path` only names it in errors. pub fn parse_macho_entitlements(path: &Path, data: &[u8]) -> Result<Option<Dictionary>> { let slices = match fat::parse_fat_arches(path, data)? { Some(mut arches) => { // iOS runs the arm64 slice, so its entitlements are the ones that matter. arches.sort_by_key(|arch| !arch.is_arm64()); arches.iter().map(|arch| &data[arch.range()]).collect() } None => vec![data], }; slices .into_iter() .map(|slice| parse_thin_entitlements(path, slice)) .find_map(Result::transpose) .transpose() } fn parse_thin_entitlements(path: &Path, data: &[u8]) -> Result<Option<Dictionary>> { let malformed = || CodeSignError::macho(path, "the code signature is malformed"); let blobs = MachOHeader::parse(path, data)? .code_signature(path, data)? .map(|superblob| parse_superblob(superblob).ok_or_else(malformed)) .transpose()?; blobs .into_iter() .flatten() .find(|(slot, _)| *slot == CSSLOT_ENTITLEMENTS) .map(|(_, blob)| match blob.split_at_checked(8) { Some((header, xml)) if BigEndian::read_u32(header) == CSMAGIC_EMBEDDED_ENTITLEMENTS => { Ok(plist::from_bytes(xml)?) } _ => Err(malformed()), }) .transpose() } fn sign_macho( path: &Path, data: Cow<'_, [u8]>, config: &MachOSigningConfig<'_>, ) -> Result<Vec<u8>> { match fat::parse_fat_arches(path, &data)? { Some(arches) => { let slices: Result<Vec<_>> = arches .par_iter() .map(|arch| sign_thin(path, data[arch.range()].to_vec(), config)) .collect(); fat::build_fat(path, &arches, &slices?) } None => sign_thin(path, data.into_owned(), config), } } fn sign_thin(path: &Path, data: Vec<u8>, config: &MachOSigningConfig<'_>) -> Result<Vec<u8>> { let mut macho = ThinMachO::parse(path, data)?; let is_executable = macho.header.is_executable(); let blobs = SignatureBlobs::new(config.entitlements, is_executable)?; let (text_offset, text_size) = macho.header.text_segment; let executable_segment = ExecutableSegment { base: text_offset, limit: text_size, flags: entitlement_exec_flags(config.entitlements, is_executable), }; // The reservation is decided before signing, since the signature's size is part of the signed header. let mut cms_blob_reservation = config.cms_blob_reservation; for _ in 0..MAX_SIGNING_ATTEMPTS { let superblob_len = blobs.superblob_len( config.identifier, config.team_id, macho.code_signature_offset()?, config.cms_signer.map(|_| cms_blob_reservation), ); let code_limit = macho.reserve_code_signature(superblob_len)?; let params = CodeDirectoryParams { identifier: config.identifier, team_id: config.team_id, macho_data: &macho.data, code_limit, executable_segment, is_executable, requirements_blob: &blobs.requirements, entitlements_blob: &blobs.entitlements, der_entitlements_blob: blobs.der_entitlements.as_deref(), info_plist: config.info_plist, code_resources: config.code_resources, }; match encode_embedded_signature(¶ms, config.cms_signer, cms_blob_reservation) { Ok(signature) => { macho.write_code_signature(&signature)?; return Ok(macho.data); } Err(CodeSignError::SignatureTooLarge { actual, .. }) => { cms_blob_reservation = next_reservation(cms_blob_reservation, actual); } Err(error) => return Err(error), } } Err(CodeSignError::macho( path, "the CMS signature did not fit after repeated reservation growth", )) } fn next_reservation(current: usize, actual: usize) -> usize { let doubled = current.saturating_mul(2).max(DEFAULT_CMS_BLOB_RESERVATION); align_to(doubled.max(actual), 4096) } fn ensure_len(path: &Path, data: &[u8], required: usize, what: &str) -> Result<()> { if data.len() < required { return Err(CodeSignError::macho( path, format!( "{what} requires {required} bytes, there are only {}", data.len() ), )); } Ok(()) } fn checked_add(path: &Path, left: usize, right: usize, what: &str) -> Result<usize> { left.checked_add(right) .ok_or_else(|| CodeSignError::macho(path, format!("{what} offset overflow"))) } fn align_to(value: usize, alignment: usize) -> usize { debug_assert!(alignment.is_power_of_two()); (value + alignment - 1) & !(alignment - 1) } #[cfg(test)] pub(crate) mod tests { use super::*; use crate::signature::{CSMAGIC_EMBEDDED_SIGNATURE, CmsSigningRequest}; use byteorder::LittleEndian; use plist::Value; use thin::{ CODE_SIGNATURE_ALIGNMENT, LC_CODE_SIGNATURE, LC_SEGMENT_64, LINKEDIT_DATA_COMMAND_LEN, MH_EXECUTE, MH_MAGIC_64, SEGMENT_PAGE_SIZE, }; const CPU_TYPE_ARM64: i32 = 0x0100_000c; const CPU_TYPE_ARMV7: i32 = 0xc; const CODE_SIGNATURE_COMMAND: usize = 32 + 72 + 72; struct TestCmsSigner; impl CmsSigner for TestCmsSigner { fn sign(&self, _: CmsSigningRequest<'_>) -> Result<Vec<u8>> { Ok(vec![0x30, 0x00]) } } fn config(entitlements: &Dictionary) -> MachOSigningConfig<'_> { MachOSigningConfig::new("com.example.test", "TEAMID", entitlements, None) } fn code_signature_range(macho: &[u8], command: usize) -> (usize, usize) { ( LittleEndian::read_u32(&macho[command + 8..]) as usize, LittleEndian::read_u32(&macho[command + 12..]) as usize, ) } /// An arm64 executable with __TEXT, an empty __LINKEDIT and an empty LC_CODE_SIGNATURE. pub(crate) fn test_macho() -> Vec<u8> { let linkedit_offset = 512; let mut data = vec![0; linkedit_offset]; write_header(&mut data, 3, 72 + 72 + 16); write_segment_64(&mut data, 32, "__TEXT", 0, linkedit_offset as u64, 0); write_segment_64( &mut data, 32 + 72, "__LINKEDIT", linkedit_offset as u64, 0, 0, ); for (offset, value) in [ (0, LC_CODE_SIGNATURE), (4, LINKEDIT_DATA_COMMAND_LEN as u32), (8, linkedit_offset as u32), (12, 0), ] { LittleEndian::write_u32(&mut data[CODE_SIGNATURE_COMMAND + offset..], value); } data } /// An arm64 executable without LC_CODE_SIGNATURE, `headerpad` bytes of padding before its first section. fn macho_without_signature(headerpad: usize, section_flags: u32) -> Vec<u8> { let commands_len = 152 + 72; let content_offset = 32 + commands_len + headerpad; let linkedit_offset = 512; let mut data = vec![0; linkedit_offset + 33]; write_header(&mut data, 2, commands_len); write_segment_64(&mut data, 32, "__TEXT", 0, linkedit_offset as u64, 1); let section = 32 + 72; LittleEndian::write_u64(&mut data[section + 40..], 1); LittleEndian::write_u32(&mut data[section + 48..], content_offset as u32); LittleEndian::write_u32(&mut data[section + 64..], section_flags); write_segment_64( &mut data, 32 + 152, "__LINKEDIT", linkedit_offset as u64, 33, 0, ); data[content_offset] = 0xaa; data[linkedit_offset] = 0xbb; data } fn write_header(data: &mut [u8], command_count: u32, commands_len: usize) { LittleEndian::write_u32(&mut data[0..], MH_MAGIC_64); LittleEndian::write_i32(&mut data[4..], CPU_TYPE_ARM64); LittleEndian::write_u32(&mut data[12..], MH_EXECUTE); LittleEndian::write_u32(&mut data[16..], command_count); LittleEndian::write_u32(&mut data[20..], commands_len as u32); } fn write_segment_64( data: &mut [u8], offset: usize, name: &str, fileoff: u64, filesize: u64, section_count: u32, ) { LittleEndian::write_u32(&mut data[offset..], LC_SEGMENT_64); LittleEndian::write_u32(&mut data[offset + 4..], 72 + 80 * section_count); data[offset + 8..offset + 8 + name.len()].copy_from_slice(name.as_bytes()); LittleEndian::write_u64( &mut data[offset + 32..], filesize.next_multiple_of(SEGMENT_PAGE_SIZE), ); LittleEndian::write_u64(&mut data[offset + 40..], fileoff); LittleEndian::write_u64(&mut data[offset + 48..], filesize); LittleEndian::write_u32(&mut data[offset + 64..], section_count); } fn fat(slices: &[(i32, &[u8])]) -> Vec<u8> { let mut data = vec![0; 0x4000]; BigEndian::write_u32(&mut data[0..], fat::FAT_MAGIC); BigEndian::write_u32(&mut data[4..], slices.len() as u32); for (index, (cpu_type, slice)) in slices.iter().enumerate() { let record = 8 + index * 20; let offset = data.len().next_multiple_of(0x4000); BigEndian::write_i32(&mut data[record..], *cpu_type); BigEndian::write_u32(&mut data[record + 8..], offset as u32); BigEndian::write_u32(&mut data[record + 12..], slice.len() as u32); BigEndian::write_u32(&mut data[record + 16..], 14); data.resize(offset, 0); data.extend_from_slice(slice); } data } #[test] fn signs_thin_macho_by_growing_existing_code_signature() { let entitlements = Dictionary::new(); let signed = sign_macho_data( Path::new("TestBinary"), &test_macho(), &config(&entitlements), ) .unwrap(); let (dataoff, datasize) = code_signature_range(&signed, CODE_SIGNATURE_COMMAND); assert!(datasize > 0); assert_eq!(signed.len(), dataoff + datasize); assert_eq!( BigEndian::read_u32(&signed[dataoff..]), CSMAGIC_EMBEDDED_SIGNATURE ); } #[test] fn signature_growth_keeps_linkedit_vmsize_large_enough() { let mut macho = test_macho(); let linkedit_command = 32 + 72; let linkedit_offset = 512; let old_signature_size = 1; let dataoff = linkedit_offset + SEGMENT_PAGE_SIZE as usize - old_signature_size; macho.resize(dataoff + old_signature_size, 0); write_segment_64( &mut macho, linkedit_command, "__LINKEDIT", linkedit_offset as u64, SEGMENT_PAGE_SIZE, 0, ); LittleEndian::write_u32(&mut macho[CODE_SIGNATURE_COMMAND + 8..], dataoff as u32); LittleEndian::write_u32( &mut macho[CODE_SIGNATURE_COMMAND + 12..], old_signature_size as u32, ); let entitlements = Dictionary::new(); let signed = sign_macho_data( Path::new("PageBoundaryBinary"), &macho, &config(&entitlements), ) .unwrap(); let filesize = LittleEndian::read_u64(&signed[linkedit_command + 48..]); let vmsize = LittleEndian::read_u64(&signed[linkedit_command + 32..]); assert!(filesize > SEGMENT_PAGE_SIZE); assert_eq!(vmsize, filesize.next_multiple_of(SEGMENT_PAGE_SIZE)); } #[test] fn cms_reservation_is_zero_padding_after_superblob() { let entitlements = Dictionary::new(); let config = MachOSigningConfig { cms_signer: Some(&TestCmsSigner), cms_blob_reservation: 1024, ..config(&entitlements) }; let signed = sign_macho_data(Path::new("TestBinary"), &test_macho(), &config).unwrap(); let (dataoff, datasize) = code_signature_range(&signed, CODE_SIGNATURE_COMMAND); let superblob_len = BigEndian::read_u32(&signed[dataoff + 4..]) as usize; assert!(superblob_len < datasize); assert!( signed[dataoff + superblob_len..dataoff + datasize] .iter() .all(|byte| *byte == 0) ); } #[test] fn empty_signature_command_is_planned_from_where_the_signature_lands() { // dataoff is stale: the signature goes after __LINKEDIT, the code limit has to follow. let mut macho = test_macho(); macho.resize(4 * 4096, 0x55); write_segment_64(&mut macho, 32, "__TEXT", 0, 4 * 4096, 0); write_segment_64(&mut macho, 32 + 72, "__LINKEDIT", 4 * 4096, 0, 0); LittleEndian::write_u32(&mut macho[CODE_SIGNATURE_COMMAND + 8..], 0); let entitlements = Dictionary::new(); let signed = sign_macho_data(Path::new("StaleBinary"), &macho, &config(&entitlements)).unwrap(); let (dataoff, datasize) = code_signature_range(&signed, CODE_SIGNATURE_COMMAND); assert_eq!(dataoff, 4 * 4096); assert_eq!(signed.len(), dataoff + datasize); } #[test] fn allocates_code_signature_command_when_headerpad_exists() { let entitlements = Dictionary::new(); let signed = sign_macho_data( Path::new("HeaderPaddedBinary"), &macho_without_signature(128, 0), &config(&entitlements), ) .unwrap(); let command = 32 + 152 + 72; assert_eq!( LittleEndian::read_u32(&signed[command..]), LC_CODE_SIGNATURE ); let (dataoff, datasize) = code_signature_range(&signed, command); let linkedit_filesize = LittleEndian::read_u64(&signed[32 + 152 + 48..]); assert_eq!(dataoff % CODE_SIGNATURE_ALIGNMENT, 0); assert!(datasize > 0); assert_eq!(signed.len(), dataoff + datasize); assert_eq!(linkedit_filesize as usize, signed.len() - 512); assert_eq!( BigEndian::read_u32(&signed[dataoff..]), CSMAGIC_EMBEDDED_SIGNATURE ); } #[test] fn refuses_to_add_code_signature_command_without_headerpad() { let entitlements = Dictionary::new(); let error = sign_macho_data( Path::new("TightBinary"), &macho_without_signature(0, 0), &config(&entitlements), ) .unwrap_err(); assert!(matches!( error, CodeSignError::NeedsCodeSignatureAllocation { .. } )); } #[test] fn zerofill_sections_do_not_end_the_header_padding() { // section_64's flags sit at +64: a zerofill section right after the commands takes no file space. const S_ZEROFILL: u32 = 0x1; let mut macho = macho_without_signature(0, S_ZEROFILL); macho[32 + 152 + 72] = 0; let entitlements = Dictionary::new(); sign_macho_data(Path::new("ZerofillBinary"), &macho, &config(&entitlements)).unwrap(); } #[test] fn linkedit_overlapping_the_load_commands_is_an_error() { let mut macho = test_macho(); write_segment_64(&mut macho, 32 + 72, "__LINKEDIT", 0, 0, 0); LittleEndian::write_u32(&mut macho[CODE_SIGNATURE_COMMAND + 8..], 0); let entitlements = Dictionary::new(); let error = sign_macho_data(Path::new("Overlapping"), &macho, &config(&entitlements)).unwrap_err(); assert!(matches!(error, CodeSignError::MachO { .. })); } #[test] fn signatures_past_the_end_of_the_file_are_an_error() { let mut macho = test_macho(); LittleEndian::write_u32(&mut macho[CODE_SIGNATURE_COMMAND + 8..], 0xffff_ff00); LittleEndian::write_u32(&mut macho[CODE_SIGNATURE_COMMAND + 12..], 1); let entitlements = Dictionary::new(); let error = sign_macho_data(Path::new("FarAway"), &macho, &config(&entitlements)).unwrap_err(); assert!(matches!(error, CodeSignError::MachO { .. })); } #[test] fn huge_fat_alignments_are_an_error() { let thin = test_macho(); let mut macho = fat(&[(CPU_TYPE_ARM64, &thin)]); BigEndian::write_u32(&mut macho[8 + 16..], 63); let entitlements = Dictionary::new(); let error = sign_macho_data(Path::new("Aligned"), &macho, &config(&entitlements)).unwrap_err(); assert!(matches!(error, CodeSignError::MachO { .. })); } #[test] fn reads_entitlements_back_from_thin_and_fat_signatures() { let entitlements = |bundle_id: &str| { Dictionary::from_iter([ ( "application-identifier", Value::from(format!("TEAMID.{bundle_id}")), ), ("get-task-allow", Value::Boolean(true)), ]) }; let (arm64, armv7) = (entitlements("arm64"), entitlements("armv7")); let path = Path::new("Entitled"); let sign = |entitlements| sign_macho_data(path, &test_macho(), &config(entitlements)).unwrap(); let (arm64_slice, armv7_slice) = (sign(&arm64), sign(&armv7)); let fat_signed = sign_macho_data( path, &fat(&[ (CPU_TYPE_ARM64, &test_macho()), (CPU_TYPE_ARM64, &test_macho()), ]), &config(&arm64), ) .unwrap(); assert_eq!( parse_macho_entitlements(path, &arm64_slice).unwrap(), Some(arm64.clone()) ); assert_eq!( parse_macho_entitlements(path, &fat_signed).unwrap(), Some(arm64.clone()) ); // The arm64 slice wins even when it comes second. let mixed = fat(&[ (CPU_TYPE_ARMV7, &armv7_slice), (CPU_TYPE_ARM64, &arm64_slice), ]); assert_eq!(parse_macho_entitlements(path, &mixed).unwrap(), Some(arm64)); let unsigned_arm64 = fat(&[ (CPU_TYPE_ARMV7, &armv7_slice), (CPU_TYPE_ARM64, &test_macho()), ]); assert_eq!( parse_macho_entitlements(path, &unsigned_arm64).unwrap(), Some(armv7) ); assert_eq!(parse_macho_entitlements(path, &test_macho()).unwrap(), None); assert!(parse_macho_entitlements(path, b"nope").is_err()); } }