use {
crate::{
error::AppleCodesignError,
macho::{
read_and_validate_blob_header, Blob, CodeSigningMagic, CodeSigningSlot, Digest,
DigestType,
},
},
scroll::{IOwrite, Pread},
std::{borrow::Cow, collections::HashMap, io::Write, str::FromStr},
};
bitflags::bitflags! {
pub struct CodeSignatureFlags: u32 {
const HOST = 0x0001;
const ADHOC = 0x0002;
const FORCE_HARD = 0x0100;
const FORCE_KILL = 0x0200;
const FORCE_EXPIRATION = 0x0400;
const RESTRICT = 0x0800;
const ENFORCEMENT = 0x1000;
const LIBRARY_VALIDATION = 0x2000;
const RUNTIME = 0x10000;
const LINKER_SIGNED = 0x20000;
}
}
impl FromStr for CodeSignatureFlags {
type Err = AppleCodesignError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"host" => Ok(Self::HOST),
"hard" => Ok(Self::FORCE_HARD),
"kill" => Ok(Self::FORCE_KILL),
"expires" => Ok(Self::FORCE_EXPIRATION),
"library" => Ok(Self::LIBRARY_VALIDATION),
"runtime" => Ok(Self::RUNTIME),
"linker-signed" => Ok(Self::LINKER_SIGNED),
_ => Err(AppleCodesignError::CodeSignatureUnknownFlag(s.to_string())),
}
}
}
impl CodeSignatureFlags {
pub fn from_strs(s: &[&str]) -> Result<CodeSignatureFlags, AppleCodesignError> {
let mut flags = CodeSignatureFlags::empty();
for s in s {
flags |= Self::from_str(s)?;
}
Ok(flags)
}
}
bitflags::bitflags! {
pub struct ExecutableSegmentFlags: u64 {
const MAIN_BINARY = 0x0001;
const ALLOW_UNSIGNED = 0x0010;
const DEBUGGER = 0x0020;
const JIT = 0x0040;
const SKIP_LIBRARY_VALIDATION = 0x0080;
const CAN_LOAD_CD_HASH = 0x0100;
const CAN_EXEC_CD_HASH = 0x0200;
}
}
impl FromStr for ExecutableSegmentFlags {
type Err = AppleCodesignError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"main-binary" => Ok(Self::MAIN_BINARY),
"allow-unsigned" => Ok(Self::ALLOW_UNSIGNED),
"debugger" => Ok(Self::DEBUGGER),
"jit" => Ok(Self::JIT),
"skip-library-validation" => Ok(Self::SKIP_LIBRARY_VALIDATION),
"can-load-cd-hash" => Ok(Self::CAN_LOAD_CD_HASH),
"can-exec-cd-hash" => Ok(Self::CAN_EXEC_CD_HASH),
_ => Err(AppleCodesignError::ExecutableSegmentUnknownFlag(
s.to_string(),
)),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[repr(u32)]
pub enum CodeDirectoryVersion {
Initial = 0x20000,
SupportsScatter = 0x20100,
SupportsTeamId = 0x20200,
SupportsCodeLimit64 = 0x20300,
SupportsExecutableSegment = 0x20400,
SupportsRuntime = 0x20500,
SupportsLinkage = 0x20600,
}
fn get_hashes(data: &[u8], offset: usize, count: usize, hash_size: usize) -> Vec<Digest<'_>> {
data[offset..offset + (count * hash_size)]
.chunks(hash_size)
.map(|data| Digest { data: data.into() })
.collect()
}
#[derive(Debug)]
pub struct CodeDirectoryBlob<'a> {
pub version: u32,
pub flags: CodeSignatureFlags,
pub code_limit: u32,
pub hash_size: u8,
pub hash_type: DigestType,
pub platform: u8,
pub page_size: u32,
pub spare2: u32,
pub scatter_offset: Option<u32>,
pub spare3: Option<u32>,
pub code_limit_64: Option<u64>,
pub exec_seg_base: Option<u64>,
pub exec_seg_limit: Option<u64>,
pub exec_seg_flags: Option<ExecutableSegmentFlags>,
pub runtime: Option<u32>,
pub pre_encrypt_offset: Option<u32>,
pub linkage_hash_type: Option<u8>,
pub linkage_truncated: Option<u8>,
pub spare4: Option<u16>,
pub linkage_offset: Option<u32>,
pub linkage_size: Option<u32>,
pub ident: Cow<'a, str>,
pub team_name: Option<Cow<'a, str>>,
pub code_hashes: Vec<Digest<'a>>,
pub special_hashes: HashMap<CodeSigningSlot, Digest<'a>>,
}
impl<'a> Blob<'a> for CodeDirectoryBlob<'a> {
fn magic() -> u32 {
u32::from(CodeSigningMagic::CodeDirectory)
}
fn from_blob_bytes(data: &'a [u8]) -> Result<Self, AppleCodesignError> {
read_and_validate_blob_header(data, Self::magic(), "code directory blob")?;
let offset = &mut 8;
let version = data.gread_with(offset, scroll::BE)?;
let flags = data.gread_with::<u32>(offset, scroll::BE)?;
let flags = unsafe { CodeSignatureFlags::from_bits_unchecked(flags) };
assert_eq!(*offset, 0x10);
let hash_offset = data.gread_with::<u32>(offset, scroll::BE)?;
let ident_offset = data.gread_with::<u32>(offset, scroll::BE)?;
let n_special_slots = data.gread_with::<u32>(offset, scroll::BE)?;
let n_code_slots = data.gread_with::<u32>(offset, scroll::BE)?;
assert_eq!(*offset, 0x20);
let code_limit = data.gread_with(offset, scroll::BE)?;
let hash_size = data.gread_with(offset, scroll::BE)?;
let hash_type = data.gread_with::<u8>(offset, scroll::BE)?.into();
let platform = data.gread_with(offset, scroll::BE)?;
let page_size = data.gread_with::<u8>(offset, scroll::BE)?;
let page_size = 2u32.pow(page_size as u32);
let spare2 = data.gread_with(offset, scroll::BE)?;
let scatter_offset = if version >= CodeDirectoryVersion::SupportsScatter as u32 {
let v = data.gread_with(offset, scroll::BE)?;
if v != 0 {
Some(v)
} else {
None
}
} else {
None
};
let team_offset = if version >= CodeDirectoryVersion::SupportsTeamId as u32 {
assert_eq!(*offset, 0x30);
let v = data.gread_with::<u32>(offset, scroll::BE)?;
if v != 0 {
Some(v)
} else {
None
}
} else {
None
};
let (spare3, code_limit_64) = if version >= CodeDirectoryVersion::SupportsCodeLimit64 as u32
{
(
Some(data.gread_with(offset, scroll::BE)?),
Some(data.gread_with(offset, scroll::BE)?),
)
} else {
(None, None)
};
let (exec_seg_base, exec_seg_limit, exec_seg_flags) =
if version >= CodeDirectoryVersion::SupportsExecutableSegment as u32 {
assert_eq!(*offset, 0x40);
(
Some(data.gread_with(offset, scroll::BE)?),
Some(data.gread_with(offset, scroll::BE)?),
Some(data.gread_with::<u64>(offset, scroll::BE)?),
)
} else {
(None, None, None)
};
let exec_seg_flags = exec_seg_flags
.map(|flags| unsafe { ExecutableSegmentFlags::from_bits_unchecked(flags) });
let (runtime, pre_encrypt_offset) =
if version >= CodeDirectoryVersion::SupportsRuntime as u32 {
assert_eq!(*offset, 0x58);
(
Some(data.gread_with(offset, scroll::BE)?),
Some(data.gread_with(offset, scroll::BE)?),
)
} else {
(None, None)
};
let (linkage_hash_type, linkage_truncated, spare4, linkage_offset, linkage_size) =
if version >= CodeDirectoryVersion::SupportsLinkage as u32 {
assert_eq!(*offset, 0x60);
(
Some(data.gread_with(offset, scroll::BE)?),
Some(data.gread_with(offset, scroll::BE)?),
Some(data.gread_with(offset, scroll::BE)?),
Some(data.gread_with(offset, scroll::BE)?),
Some(data.gread_with(offset, scroll::BE)?),
)
} else {
(None, None, None, None, None)
};
let ident = match data[ident_offset as usize..]
.split(|&b| b == 0)
.map(std::str::from_utf8)
.next()
{
Some(res) => {
Cow::from(res.map_err(|_| AppleCodesignError::CodeDirectoryMalformedIdentifier)?)
}
None => {
return Err(AppleCodesignError::CodeDirectoryMalformedIdentifier);
}
};
let team_name = if let Some(team_offset) = team_offset {
match data[team_offset as usize..]
.split(|&b| b == 0)
.map(std::str::from_utf8)
.next()
{
Some(res) => {
Some(Cow::from(res.map_err(|_| {
AppleCodesignError::CodeDirectoryMalformedTeam
})?))
}
None => {
return Err(AppleCodesignError::CodeDirectoryMalformedTeam);
}
}
} else {
None
};
let code_hashes = get_hashes(
data,
hash_offset as usize,
n_code_slots as usize,
hash_size as usize,
);
let special_hashes = get_hashes(
data,
(hash_offset - (hash_size as u32 * n_special_slots)) as usize,
n_special_slots as usize,
hash_size as usize,
)
.into_iter()
.enumerate()
.map(|(i, h)| (CodeSigningSlot::from(n_special_slots - i as u32), h))
.collect();
Ok(Self {
version,
flags,
code_limit,
hash_size,
hash_type,
platform,
page_size,
spare2,
scatter_offset,
spare3,
code_limit_64,
exec_seg_base,
exec_seg_limit,
exec_seg_flags,
runtime,
pre_encrypt_offset,
linkage_hash_type,
linkage_truncated,
spare4,
linkage_offset,
linkage_size,
ident,
team_name,
code_hashes,
special_hashes,
})
}
fn serialize_payload(&self) -> Result<Vec<u8>, AppleCodesignError> {
let mut cursor = std::io::Cursor::new(Vec::<u8>::new());
cursor.iowrite_with(self.version, scroll::BE)?;
cursor.iowrite_with(self.flags.bits, scroll::BE)?;
let hash_offset_cursor_position = cursor.position();
cursor.iowrite_with(0u32, scroll::BE)?;
let ident_offset_cursor_position = cursor.position();
cursor.iowrite_with(0u32, scroll::BE)?;
assert_eq!(cursor.position(), 0x10);
let highest_slot = self
.special_hashes
.keys()
.map(|slot| u32::from(*slot))
.max()
.unwrap_or(0);
cursor.iowrite_with(highest_slot as u32, scroll::BE)?;
cursor.iowrite_with(self.code_hashes.len() as u32, scroll::BE)?;
cursor.iowrite_with(self.code_limit, scroll::BE)?;
cursor.iowrite_with(self.hash_size, scroll::BE)?;
cursor.iowrite_with(u8::from(self.hash_type), scroll::BE)?;
cursor.iowrite_with(self.platform, scroll::BE)?;
cursor.iowrite_with(self.page_size.trailing_zeros() as u8, scroll::BE)?;
assert_eq!(cursor.position(), 0x20);
cursor.iowrite_with(self.spare2, scroll::BE)?;
let mut scatter_offset_cursor_position = None;
let mut team_offset_cursor_position = None;
if self.version >= CodeDirectoryVersion::SupportsScatter as u32 {
scatter_offset_cursor_position = Some(cursor.position());
cursor.iowrite_with(self.scatter_offset.unwrap_or(0), scroll::BE)?;
if self.version >= CodeDirectoryVersion::SupportsTeamId as u32 {
team_offset_cursor_position = Some(cursor.position());
cursor.iowrite_with(0u32, scroll::BE)?;
if self.version >= CodeDirectoryVersion::SupportsCodeLimit64 as u32 {
cursor.iowrite_with(self.spare3.unwrap_or(0), scroll::BE)?;
assert_eq!(cursor.position(), 0x30);
cursor.iowrite_with(self.code_limit_64.unwrap_or(0), scroll::BE)?;
if self.version >= CodeDirectoryVersion::SupportsExecutableSegment as u32 {
cursor.iowrite_with(self.exec_seg_base.unwrap_or(0), scroll::BE)?;
assert_eq!(cursor.position(), 0x40);
cursor.iowrite_with(self.exec_seg_limit.unwrap_or(0), scroll::BE)?;
cursor.iowrite_with(
self.exec_seg_flags
.unwrap_or_else(ExecutableSegmentFlags::empty)
.bits,
scroll::BE,
)?;
if self.version >= CodeDirectoryVersion::SupportsRuntime as u32 {
assert_eq!(cursor.position(), 0x50);
cursor.iowrite_with(self.runtime.unwrap_or(0), scroll::BE)?;
cursor
.iowrite_with(self.pre_encrypt_offset.unwrap_or(0), scroll::BE)?;
if self.version >= CodeDirectoryVersion::SupportsLinkage as u32 {
cursor.iowrite_with(
self.linkage_hash_type.unwrap_or(0),
scroll::BE,
)?;
cursor.iowrite_with(
self.linkage_truncated.unwrap_or(0),
scroll::BE,
)?;
cursor.iowrite_with(self.spare4.unwrap_or(0), scroll::BE)?;
cursor
.iowrite_with(self.linkage_offset.unwrap_or(0), scroll::BE)?;
assert_eq!(cursor.position(), 0x60);
cursor.iowrite_with(self.linkage_size.unwrap_or(0), scroll::BE)?;
}
}
}
}
}
}
let identity_offset = cursor.position();
cursor.write_all(self.ident.as_bytes())?;
cursor.write_all(b"\0")?;
let team_offset = cursor.position();
if team_offset_cursor_position.is_some() {
if let Some(team_name) = &self.team_name {
cursor.write_all(team_name.as_bytes())?;
cursor.write_all(b"\0")?;
}
}
for slot_index in (1..highest_slot + 1).rev() {
if let Some(hash) = self.special_hashes.get(&CodeSigningSlot::from(slot_index)) {
cursor.write_all(&hash.data)?;
} else {
cursor.write_all(&b"\0".repeat(self.hash_size as usize))?;
}
}
let code_hashes_start_offset = cursor.position();
for hash in &self.code_hashes {
cursor.write_all(&hash.data)?;
}
cursor.set_position(hash_offset_cursor_position);
cursor.iowrite_with(code_hashes_start_offset as u32 + 8, scroll::BE)?;
cursor.set_position(ident_offset_cursor_position);
cursor.iowrite_with(identity_offset as u32 + 8, scroll::BE)?;
if scatter_offset_cursor_position.is_some() && self.scatter_offset.is_some() {
return Err(AppleCodesignError::Unimplemented("scatter offset"));
}
if let Some(offset) = team_offset_cursor_position {
if self.team_name.is_some() {
cursor.set_position(offset);
cursor.iowrite_with(team_offset as u32 + 8, scroll::BE)?;
}
}
Ok(cursor.into_inner())
}
}
impl<'a> CodeDirectoryBlob<'a> {
pub fn adjust_version(&mut self) -> u32 {
let old_version = self.version;
let mut minimum_version = CodeDirectoryVersion::Initial;
if self.scatter_offset.is_some() {
minimum_version = CodeDirectoryVersion::SupportsScatter;
}
if self.team_name.is_some() {
minimum_version = CodeDirectoryVersion::SupportsTeamId;
}
if self.spare3.is_some() || self.code_limit_64.is_some() {
minimum_version = CodeDirectoryVersion::SupportsCodeLimit64;
}
if self.exec_seg_base.is_some()
|| self.exec_seg_limit.is_some()
|| self.exec_seg_flags.is_some()
{
minimum_version = CodeDirectoryVersion::SupportsExecutableSegment;
}
if self.runtime.is_some() || self.pre_encrypt_offset.is_some() {
minimum_version = CodeDirectoryVersion::SupportsRuntime;
}
if self.linkage_hash_type.is_some()
|| self.linkage_truncated.is_some()
|| self.spare4.is_some()
|| self.linkage_offset.is_some()
|| self.linkage_size.is_some()
{
minimum_version = CodeDirectoryVersion::SupportsLinkage;
}
self.version = minimum_version as u32;
old_version
}
pub fn clear_newer_fields(&mut self) {
if self.version < CodeDirectoryVersion::SupportsScatter as u32 {
self.scatter_offset = None;
}
if self.version < CodeDirectoryVersion::SupportsTeamId as u32 {
self.team_name = None;
}
if self.version < CodeDirectoryVersion::SupportsCodeLimit64 as u32 {
self.spare3 = None;
self.code_limit_64 = None;
}
if self.version < CodeDirectoryVersion::SupportsExecutableSegment as u32 {
self.exec_seg_base = None;
self.exec_seg_limit = None;
self.exec_seg_flags = None;
}
if self.version < CodeDirectoryVersion::SupportsRuntime as u32 {
self.runtime = None;
self.pre_encrypt_offset = None;
}
if self.version < CodeDirectoryVersion::SupportsLinkage as u32 {
self.linkage_hash_type = None;
self.linkage_truncated = None;
self.spare4 = None;
self.linkage_offset = None;
self.linkage_size = None;
}
}
pub fn to_owned(&self) -> CodeDirectoryBlob<'static> {
CodeDirectoryBlob {
version: self.version,
flags: self.flags,
code_limit: self.code_limit,
hash_size: self.hash_size,
hash_type: self.hash_type,
platform: self.platform,
page_size: self.page_size,
spare2: self.spare2,
scatter_offset: self.scatter_offset,
spare3: self.spare3,
code_limit_64: self.code_limit_64,
exec_seg_base: self.exec_seg_base,
exec_seg_limit: self.exec_seg_limit,
exec_seg_flags: self.exec_seg_flags,
runtime: self.runtime,
pre_encrypt_offset: self.pre_encrypt_offset,
linkage_hash_type: self.linkage_hash_type,
linkage_truncated: self.linkage_truncated,
spare4: self.spare4,
linkage_offset: self.linkage_offset,
linkage_size: self.linkage_size,
ident: Cow::Owned(self.ident.clone().into_owned()),
team_name: self
.team_name
.as_ref()
.map(|x| Cow::Owned(x.clone().into_owned())),
code_hashes: self
.code_hashes
.iter()
.map(|h| h.to_owned())
.collect::<Vec<_>>(),
special_hashes: self
.special_hashes
.iter()
.map(|(k, v)| (k.to_owned(), v.to_owned()))
.collect::<HashMap<_, _>>(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn code_signature_flags_from_str() {
assert_eq!(
CodeSignatureFlags::from_str("host").unwrap(),
CodeSignatureFlags::HOST
);
assert_eq!(
CodeSignatureFlags::from_str("hard").unwrap(),
CodeSignatureFlags::FORCE_HARD
);
assert_eq!(
CodeSignatureFlags::from_str("kill").unwrap(),
CodeSignatureFlags::FORCE_KILL
);
assert_eq!(
CodeSignatureFlags::from_str("expires").unwrap(),
CodeSignatureFlags::FORCE_EXPIRATION
);
assert_eq!(
CodeSignatureFlags::from_str("library").unwrap(),
CodeSignatureFlags::LIBRARY_VALIDATION
);
assert_eq!(
CodeSignatureFlags::from_str("runtime").unwrap(),
CodeSignatureFlags::RUNTIME
);
assert_eq!(
CodeSignatureFlags::from_str("linker-signed").unwrap(),
CodeSignatureFlags::LINKER_SIGNED
);
}
}