use {
crate::{
cryptography::{Digest, DigestType},
embedded_signature::{
read_and_validate_blob_header, Blob, CodeSigningMagic, CodeSigningSlot,
},
error::AppleCodesignError,
macho::{MachoTarget, Platform},
},
scroll::{IOwrite, Pread},
semver::Version,
std::{borrow::Cow, collections::BTreeMap, io::Write, str::FromStr},
};
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
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 all_user_configurable() -> [&'static str; 7] {
[
"host",
"hard",
"kill",
"expires",
"library",
"runtime",
"linker-signed",
]
}
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! {
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
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, Eq, PartialEq)]
#[repr(u32)]
pub enum CodeDirectoryVersion {
Initial = 0x20000,
SupportsScatter = 0x20100,
SupportsTeamId = 0x20200,
SupportsCodeLimit64 = 0x20300,
SupportsExecutableSegment = 0x20400,
SupportsRuntime = 0x20500,
SupportsLinkage = 0x20600,
}
#[repr(C)]
pub struct Scatter {
count: u32,
base: u32,
target_offset: u64,
spare: u64,
}
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, Default)]
pub struct CodeDirectoryBlob<'a> {
pub version: u32,
pub flags: CodeSignatureFlags,
pub code_limit: u32,
pub digest_size: u8,
pub digest_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_digests: Vec<Digest<'a>>,
pub special_digests: BTreeMap<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 = CodeSignatureFlags::from_bits_retain(flags);
assert_eq!(*offset, 0x10);
let digest_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 digest_size = data.gread_with(offset, scroll::BE)?;
let digest_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(ExecutableSegmentFlags::from_bits_retain);
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_digests = get_hashes(
data,
digest_offset as usize,
n_code_slots as usize,
digest_size as usize,
);
let special_digests = get_hashes(
data,
(digest_offset - (digest_size as u32 * n_special_slots)) as usize,
n_special_slots as usize,
digest_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,
digest_size,
digest_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_digests,
special_digests,
})
}
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 digest_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_digests
.keys()
.map(|slot| u32::from(*slot))
.max()
.unwrap_or(0);
cursor.iowrite_with(highest_slot, scroll::BE)?;
cursor.iowrite_with(self.code_digests.len() as u32, scroll::BE)?;
cursor.iowrite_with(self.code_limit, scroll::BE)?;
cursor.iowrite_with(self.digest_size, scroll::BE)?;
cursor.iowrite_with(u8::from(self.digest_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() {
let slot = CodeSigningSlot::from(slot_index);
assert!(
slot.is_code_directory_specials_expressible(),
"slot is expressible in code directory special digests"
);
if let Some(digest) = self.special_digests.get(&slot) {
assert_eq!(
digest.data.len(),
self.digest_size as usize,
"special slot digest length matches expected length"
);
cursor.write_all(&digest.data)?;
} else {
cursor.write_all(&b"\0".repeat(self.digest_size as usize))?;
}
}
let code_digests_start_offset = cursor.position();
for digest in &self.code_digests {
cursor.write_all(&digest.data)?;
}
cursor.set_position(digest_offset_cursor_position);
cursor.iowrite_with(code_digests_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 slot_digests(&self) -> &BTreeMap<CodeSigningSlot, Digest<'a>> {
&self.special_digests
}
pub fn slot_digest(&self, slot: CodeSigningSlot) -> Option<&Digest<'a>> {
self.special_digests.get(&slot)
}
pub fn set_slot_digest(
&mut self,
slot: CodeSigningSlot,
digest: impl Into<Digest<'a>>,
) -> Result<(), AppleCodesignError> {
if !slot.is_code_directory_specials_expressible() {
return Err(AppleCodesignError::LogicError(format!(
"slot {slot:?} cannot have its digest expressed on code directories"
)));
}
let digest = digest.into();
if digest.data.len() != self.digest_size as usize {
return Err(AppleCodesignError::LogicError(format!(
"attempt to assign digest for slot {:?} whose length {} does not match code directory digest length {}",
slot, digest.data.len(), self.digest_size
)));
}
self.special_digests.insert(slot, digest);
Ok(())
}
pub fn adjust_version(&mut self, target: Option<MachoTarget>) -> 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;
}
if let Some(target) = target {
let target_minimum = match target.platform {
Platform::IOs | Platform::IosSimulator => {
if target.minimum_os_version >= Version::new(15, 0, 0) {
CodeDirectoryVersion::SupportsExecutableSegment
} else {
CodeDirectoryVersion::Initial
}
}
Platform::MacOs => {
if target.minimum_os_version >= Version::new(12, 0, 0) {
CodeDirectoryVersion::SupportsExecutableSegment
} else {
CodeDirectoryVersion::Initial
}
}
_ => CodeDirectoryVersion::Initial,
};
if target_minimum as u32 > minimum_version as u32 {
minimum_version = target_minimum;
}
}
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,
digest_size: self.digest_size,
digest_type: self.digest_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_digests: self
.code_digests
.iter()
.map(|h| h.to_owned())
.collect::<Vec<_>>(),
special_digests: self
.special_digests
.iter()
.map(|(k, v)| (k.to_owned(), v.to_owned()))
.collect::<BTreeMap<_, _>>(),
}
}
}
#[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
);
}
}