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
signature.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
signature.rs
Blame
History
Raw
use crate::error::{CodeSignError, Result}; use byteorder::{BigEndian, ByteOrder}; use plist::Dictionary; use rayon::prelude::*; use sha1::Digest as _; use std::path::Path; mod entitlements; pub use entitlements::*; pub const CSSLOT_CODEDIRECTORY: u32 = 0; pub const CSSLOT_REQUIREMENTS: u32 = 2; pub const CSSLOT_ENTITLEMENTS: u32 = 5; pub const CSSLOT_DER_ENTITLEMENTS: u32 = 7; pub const CSSLOT_ALTERNATE_CODEDIRECTORIES: u32 = 0x1000; pub const CSSLOT_SIGNATURESLOT: u32 = 0x10000; pub const CSMAGIC_BLOBWRAPPER: u32 = 0xfade_0b01; pub const CSMAGIC_REQUIREMENTS: u32 = 0xfade_0c01; pub const CSMAGIC_CODEDIRECTORY: u32 = 0xfade_0c02; pub const CSMAGIC_EMBEDDED_SIGNATURE: u32 = 0xfade_0cc0; pub const CSMAGIC_EMBEDDED_ENTITLEMENTS: u32 = 0xfade_7171; pub const CSMAGIC_EMBEDDED_DER_ENTITLEMENTS: u32 = 0xfade_7172; pub const CS_EXECSEG_MAIN_BINARY: u64 = 0x1; pub const CS_EXECSEG_ALLOW_UNSIGNED: u64 = 0x10; pub const CS_EXECSEG_DEBUGGER: u64 = 0x20; pub const CS_EXECSEG_JIT: u64 = 0x40; pub const CS_EXECSEG_SKIP_LV: u64 = 0x80; pub const CS_EXECSEG_CAN_LOAD_CDHASH: u64 = 0x100; pub const CS_EXECSEG_CAN_EXEC_CDHASH: u64 = 0x200; const CODEDIRECTORY_VERSION_EXECSEG: u32 = 0x20400; // Without a CMS blob nothing vouches for the CodeDirectory, which has to say so. const CS_ADHOC: u32 = 0x2; const CODEDIRECTORY_HEADER_LEN: usize = 88; const CODEDIRECTORY_PAGE_SIZE_LOG2: u8 = 12; const CODEDIRECTORY_PAGE_SIZE: usize = 1 << CODEDIRECTORY_PAGE_SIZE_LOG2; const BLOB_HEADER_LEN: usize = 8; const SUPERBLOB_HEADER_LEN: usize = 12; const SUPERBLOB_INDEX_ENTRY_LEN: usize = 8; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum HashAlgorithm { Sha1, Sha256, } impl HashAlgorithm { pub fn digest(self, bytes: &[u8]) -> Vec<u8> { match self { HashAlgorithm::Sha1 => sha1::Sha1::digest(bytes).to_vec(), HashAlgorithm::Sha256 => sha2::Sha256::digest(bytes).to_vec(), } } pub fn digest_len(self) -> usize { match self { HashAlgorithm::Sha1 => 20, HashAlgorithm::Sha256 => 32, } } pub fn code_directory_hash_type(self) -> u8 { match self { HashAlgorithm::Sha1 => 1, HashAlgorithm::Sha256 => 2, } } } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct ExecutableSegment { pub base: u64, pub limit: u64, pub flags: u64, } #[derive(Debug)] pub struct EncodedCodeDirectory<'a> { pub algorithm: HashAlgorithm, pub bytes: &'a [u8], } #[derive(Debug)] pub struct CmsSigningRequest<'a> { pub code_directories: &'a [EncodedCodeDirectory<'a>], } /// Signs the CodeDirectories of an embedded signature; the first one is the primary. pub trait CmsSigner: Sync { fn sign(&self, request: CmsSigningRequest<'_>) -> Result<Vec<u8>>; } #[derive(Debug)] pub struct CodeDirectoryParams<'a> { pub identifier: &'a str, pub team_id: &'a str, pub macho_data: &'a [u8], pub code_limit: usize, pub executable_segment: ExecutableSegment, pub is_executable: bool, pub requirements_blob: &'a [u8], pub entitlements_blob: &'a [u8], pub der_entitlements_blob: Option<&'a [u8]>, pub info_plist: Option<&'a [u8]>, pub code_resources: Option<&'a [u8]>, } pub fn empty_requirements_blob() -> Vec<u8> { blob(CSMAGIC_REQUIREMENTS, &0u32.to_be_bytes()) } pub fn entitlements_blob(xml: &[u8]) -> Vec<u8> { blob(CSMAGIC_EMBEDDED_ENTITLEMENTS, xml) } pub fn der_entitlements_blob(entitlements: &Dictionary) -> Result<Vec<u8>> { Ok(blob( CSMAGIC_EMBEDDED_DER_ENTITLEMENTS, &der_entitlements(entitlements)?, )) } /// The blobs of an embedded signature that don't depend on the code being signed. pub(crate) struct SignatureBlobs { pub requirements: Vec<u8>, pub entitlements: Vec<u8>, pub der_entitlements: Option<Vec<u8>>, } impl SignatureBlobs { pub(crate) fn new(entitlements: &Dictionary, is_executable: bool) -> Result<Self> { Ok(SignatureBlobs { requirements: empty_requirements_blob(), entitlements: entitlements_blob(&entitlements_xml(entitlements)?), // Only main executables carry the DER form, as codesign does. der_entitlements: is_executable .then(|| der_entitlements_blob(entitlements)) .transpose()?, }) } /// The size of the superblob `encode_embedded_signature` makes from these blobs, CMS reservation included. /// Like the encoder, I take the presence of DER entitlements to mean an executable. pub(crate) fn superblob_len( &self, identifier: &str, team_id: &str, code_limit: usize, cms_blob_reservation: Option<usize>, ) -> usize { let code_directories_len: usize = [HashAlgorithm::Sha1, HashAlgorithm::Sha256] .into_iter() .map(|algorithm| { CodeDirectoryLayout::new( algorithm, identifier, team_id, code_limit.div_ceil(CODEDIRECTORY_PAGE_SIZE), self.der_entitlements.is_some(), ) .len }) .sum(); let blob_count = 4 + usize::from(self.der_entitlements.is_some()) + usize::from(cms_blob_reservation.is_some()); SUPERBLOB_HEADER_LEN + blob_count * SUPERBLOB_INDEX_ENTRY_LEN + code_directories_len + self.requirements.len() + self.entitlements.len() + self.der_entitlements.as_ref().map_or(0, Vec::len) + cms_blob_reservation.unwrap_or(0) } } pub fn encode_embedded_signature( params: &CodeDirectoryParams<'_>, cms_signer: Option<&dyn CmsSigner>, cms_blob_reservation: usize, ) -> Result<Vec<u8>> { if params.code_limit > params.macho_data.len() { return Err(CodeSignError::macho( Path::new("<memory>"), format!( "code limit {} exceeds Mach-O length {}", params.code_limit, params.macho_data.len() ), )); } let code_hashes = hash_code_pages(¶ms.macho_data[..params.code_limit]); let flags = if cms_signer.is_some() { 0 } else { CS_ADHOC }; let (code_directory, alternate_code_directory) = rayon::join( || encode_code_directory(params, HashAlgorithm::Sha1, flags, &code_hashes), || encode_code_directory(params, HashAlgorithm::Sha256, flags, &code_hashes), ); let (code_directory, alternate_code_directory) = (code_directory?, alternate_code_directory?); let signature = cms_signer .map(|cms_signer| { let cms = cms_signer.sign(CmsSigningRequest { code_directories: &[ EncodedCodeDirectory { algorithm: HashAlgorithm::Sha1, bytes: &code_directory, }, EncodedCodeDirectory { algorithm: HashAlgorithm::Sha256, bytes: &alternate_code_directory, }, ], })?; signature_blob_wrapper(&cms, cms_blob_reservation) }) .transpose()?; let mut blobs = vec![ (CSSLOT_CODEDIRECTORY, code_directory.as_slice()), (CSSLOT_REQUIREMENTS, params.requirements_blob), (CSSLOT_ENTITLEMENTS, params.entitlements_blob), ]; blobs.extend( params .der_entitlements_blob .map(|der_entitlements| (CSSLOT_DER_ENTITLEMENTS, der_entitlements)), ); blobs.push(( CSSLOT_ALTERNATE_CODEDIRECTORIES, alternate_code_directory.as_slice(), )); blobs.extend( signature .as_deref() .map(|signature| (CSSLOT_SIGNATURESLOT, signature)), ); Ok(encode_superblob(&blobs)) } /// The blobs of an embedded signature superblob by slot, headers included, or None if it is malformed. pub(crate) fn parse_superblob(superblob: &[u8]) -> Option<Vec<(u32, &[u8])>> { let read_u32 = |offset: usize| { Some(BigEndian::read_u32( superblob.get(offset..offset.checked_add(4)?)?, )) }; if read_u32(0)? != CSMAGIC_EMBEDDED_SIGNATURE { return None; } (0..read_u32(8)? as usize) .map(|index| { let entry = SUPERBLOB_HEADER_LEN + index * SUPERBLOB_INDEX_ENTRY_LEN; let offset = read_u32(entry + 4)? as usize; let len = read_u32(offset.checked_add(4)?)? as usize; Some(( read_u32(entry)?, superblob.get(offset..offset.checked_add(len)?)?, )) }) .collect() } /// Where a CodeDirectory puts things, shared by its encoder and the size planning. struct CodeDirectoryLayout { hash_len: usize, special_slots: usize, team_offset: usize, hash_offset: usize, len: usize, } impl CodeDirectoryLayout { fn new( algorithm: HashAlgorithm, identifier: &str, team_id: &str, code_slots: usize, is_executable: bool, ) -> Self { let hash_len = algorithm.digest_len(); // Executables go up to slot 7 for their DER entitlements, through the unused slot 6. let special_slots = if is_executable { 7 } else { 5 }; let team_offset = CODEDIRECTORY_HEADER_LEN + identifier.len() + 1; let hash_offset = team_offset + team_id.len() + 1 + special_slots * hash_len; CodeDirectoryLayout { hash_len, special_slots, team_offset, hash_offset, len: hash_offset + code_slots * hash_len, } } } #[derive(Clone, Copy, Debug)] struct CodePageHashes { sha1: [u8; 20], sha256: [u8; 32], } fn hash_code_pages(code: &[u8]) -> Vec<CodePageHashes> { // Pages are too small to be worth a task each, batches of 64 keep rayon's overhead low. code.par_chunks(CODEDIRECTORY_PAGE_SIZE) .with_min_len(64) .map(|page| CodePageHashes { sha1: sha1::Sha1::digest(page).into(), sha256: sha2::Sha256::digest(page).into(), }) .collect() } fn encode_code_directory( params: &CodeDirectoryParams<'_>, algorithm: HashAlgorithm, flags: u32, code_hashes: &[CodePageHashes], ) -> Result<Vec<u8>> { let layout = CodeDirectoryLayout::new( algorithm, params.identifier, params.team_id, code_hashes.len(), params.is_executable, ); // Special slots are stored backwards, from the highest one down to slot 1. let mut special_slots = Vec::with_capacity(layout.special_slots); if params.is_executable { let der_entitlements = params.der_entitlements_blob.ok_or_else(|| { CodeSignError::macho( Path::new("<memory>"), "executable signatures require DER entitlements", ) })?; special_slots.extend([Some(der_entitlements), None]); } special_slots.extend([ Some(params.entitlements_blob), None, params.code_resources, Some(params.requirements_blob), params.info_plist, ]); let mut out = Vec::with_capacity(layout.len); for value in [ CSMAGIC_CODEDIRECTORY, layout.len as u32, CODEDIRECTORY_VERSION_EXECSEG, flags, layout.hash_offset as u32, CODEDIRECTORY_HEADER_LEN as u32, // identOffset layout.special_slots as u32, code_hashes.len() as u32, u32::try_from(params.code_limit).unwrap_or(0), ] { out.extend_from_slice(&value.to_be_bytes()); } out.extend_from_slice(&[ layout.hash_len as u8, algorithm.code_directory_hash_type(), 0, // platform CODEDIRECTORY_PAGE_SIZE_LOG2, ]); for value in [0, 0, layout.team_offset as u32, 0] { // spare2, scatterOffset, teamOffset, spare3 out.extend_from_slice(&value.to_be_bytes()); } // codeLimit64 is only for code that doesn't fit codeLimit. let code_limit_64 = match u32::try_from(params.code_limit) { Ok(_) => 0, Err(_) => params.code_limit as u64, }; for value in [ code_limit_64, params.executable_segment.base, params.executable_segment.limit, params.executable_segment.flags, ] { out.extend_from_slice(&value.to_be_bytes()); } out.extend_from_slice(params.identifier.as_bytes()); out.push(0); out.extend_from_slice(params.team_id.as_bytes()); out.push(0); for slot in special_slots { match slot { Some(data) => out.extend_from_slice(&algorithm.digest(data)), None => out.resize(out.len() + layout.hash_len, 0), } } for hashes in code_hashes { out.extend_from_slice(match algorithm { HashAlgorithm::Sha1 => &hashes.sha1, HashAlgorithm::Sha256 => &hashes.sha256, }); } debug_assert_eq!(out.len(), layout.len); Ok(out) } fn signature_blob_wrapper(cms: &[u8], reserved_len: usize) -> Result<Vec<u8>> { let actual_len = BLOB_HEADER_LEN + cms.len(); if actual_len > reserved_len { return Err(CodeSignError::SignatureTooLarge { actual: actual_len, reserved: reserved_len, }); } Ok(blob(CSMAGIC_BLOBWRAPPER, cms)) } /// A blob: magic, then the length of the whole blob, then the payload. fn blob(magic: u32, payload: &[u8]) -> Vec<u8> { let len = (BLOB_HEADER_LEN + payload.len()) as u32; [&magic.to_be_bytes(), &len.to_be_bytes(), payload].concat() } /// The CodeDirectory has to be first, then blobs follow their index in order. fn encode_superblob(blobs: &[(u32, &[u8])]) -> Vec<u8> { let index_len = blobs.len() * SUPERBLOB_INDEX_ENTRY_LEN; let data_len: usize = blobs.iter().map(|(_, data)| data.len()).sum(); let total_len = SUPERBLOB_HEADER_LEN + index_len + data_len; let mut out = Vec::with_capacity(total_len); for value in [ CSMAGIC_EMBEDDED_SIGNATURE, total_len as u32, blobs.len() as u32, ] { out.extend_from_slice(&value.to_be_bytes()); } let mut offset = SUPERBLOB_HEADER_LEN + index_len; for (slot, data) in blobs { out.extend_from_slice(&slot.to_be_bytes()); out.extend_from_slice(&(offset as u32).to_be_bytes()); offset += data.len(); } for (_, data) in blobs { out.extend_from_slice(data); } out } #[cfg(test)] mod tests { use super::*; #[test] fn der_entitlements_blob_reports_the_wrapped_length() { let blob = der_entitlements_blob(&Dictionary::new()).unwrap(); assert_eq!(&blob[..4], &CSMAGIC_EMBEDDED_DER_ENTITLEMENTS.to_be_bytes()); assert_eq!(BigEndian::read_u32(&blob[4..]) as usize, blob.len()); assert_eq!(&blob[8..], &[0x70, 0x05, 0x02, 0x01, 0x01, 0xb0, 0x00]); } #[test] fn cms_reservation_is_not_encoded_inside_blob_wrapper() { let cms = [0x30, 0x00]; let blob = signature_blob_wrapper(&cms, 32).unwrap(); assert_eq!(blob.len(), 10); assert_eq!(BigEndian::read_u32(&blob[4..]), 10); assert_eq!(&blob[8..10], &cms); } #[test] fn cms_reservation_is_not_counted_in_outer_superblob_length() { let cms = signature_blob_wrapper(&[0x30, 0x00], 32).unwrap(); let superblob = encode_superblob(&[ (CSSLOT_CODEDIRECTORY, &[0; 8]), (CSSLOT_SIGNATURESLOT, &cms), ]); assert_eq!( BigEndian::read_u32(&superblob[4..]) as usize, superblob.len() ); assert_eq!(superblob.len(), 46); } #[test] fn superblob_round_trips_and_rejects_bad_offsets() { let entitlements = entitlements_blob(b"<plist/>"); let superblob = encode_superblob(&[ (CSSLOT_CODEDIRECTORY, &[0; 8]), (CSSLOT_ENTITLEMENTS, &entitlements), ]); let blobs = parse_superblob(&superblob).unwrap(); assert_eq!(blobs[1], (CSSLOT_ENTITLEMENTS, entitlements.as_slice())); let mut corrupted = superblob.clone(); corrupted[24..28].copy_from_slice(&u32::MAX.to_be_bytes()); assert!(parse_superblob(&corrupted).is_none()); assert!(parse_superblob(&superblob[..20]).is_none()); } #[test] fn planned_superblob_len_matches_the_encoded_signature() { let code = vec![0xaa; 3 * CODEDIRECTORY_PAGE_SIZE + 1]; for is_executable in [true, false] { let blobs = SignatureBlobs::new(&Dictionary::new(), is_executable).unwrap(); let params = CodeDirectoryParams { identifier: "com.example.test", team_id: "TEAMID", macho_data: &code, code_limit: code.len(), executable_segment: ExecutableSegment::default(), is_executable, requirements_blob: &blobs.requirements, entitlements_blob: &blobs.entitlements, der_entitlements_blob: blobs.der_entitlements.as_deref(), info_plist: Some(b"info"), code_resources: None, }; let signature = encode_embedded_signature(¶ms, None, 0).unwrap(); assert_eq!( blobs.superblob_len("com.example.test", "TEAMID", code.len(), None), signature.len() ); } } }