use bitflags::bitflags;
use sha2::{Digest, Sha256, Sha384, Sha512};
use crate::util::{read_cstr_at, read_u32_be_at, read_u64_be_at};
pub const CSMAGIC_EMBEDDED_SIGNATURE: u32 = 0xfade_0cc0;
pub const CSMAGIC_EMBEDDED_SIGNATURE_OLD: u32 = 0xfade_0b02;
pub const CSMAGIC_CODEDIRECTORY: u32 = 0xfade_0c02;
pub const CSMAGIC_REQUIREMENT: u32 = 0xfade_0c00;
pub const CSMAGIC_REQUIREMENTS: u32 = 0xfade_0c01;
pub const CSMAGIC_EMBEDDED_ENTITLEMENTS: u32 = 0xfade_7171;
pub const CSMAGIC_EMBEDDED_DER_ENTITLEMENTS: u32 = 0xfade_7172;
pub const CSMAGIC_BLOBWRAPPER: u32 = 0xfade_0b01;
#[allow(dead_code)] pub(crate) const BLOB_HEADER_SIZE: usize = 8;
const SUPERBLOB_HEADER_SIZE: usize = 12;
const BLOB_INDEX_SIZE: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Slot {
CodeDirectory,
InfoSlot,
Requirements,
ResourceDir,
Application,
Entitlements,
DerEntitlements,
LaunchConstraintSelf,
LaunchConstraintParent,
LaunchConstraintResponsible,
LibraryConstraint,
AlternateCodeDirectory(u32),
SignatureSlot,
IdentificationSlot,
TicketSlot,
Other(u32),
}
impl Slot {
#[inline]
pub fn from_raw(raw: u32) -> Self {
match raw {
0 => Self::CodeDirectory,
1 => Self::InfoSlot,
2 => Self::Requirements,
3 => Self::ResourceDir,
4 => Self::Application,
5 => Self::Entitlements,
7 => Self::DerEntitlements,
8 => Self::LaunchConstraintSelf,
9 => Self::LaunchConstraintParent,
10 => Self::LaunchConstraintResponsible,
11 => Self::LibraryConstraint,
n if (0x1000..=0x1004).contains(&n) => {
Self::AlternateCodeDirectory(n.wrapping_sub(0x1000))
}
0x10000 => Self::SignatureSlot,
0x10001 => Self::IdentificationSlot,
0x10002 => Self::TicketSlot,
other => Self::Other(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlobIndex {
pub slot: Slot,
pub raw_slot: u32,
pub offset: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct Signature<'a> {
data: &'a [u8],
base: usize,
magic: u32,
length: u32,
count: u32,
}
impl<'a> Signature<'a> {
pub fn parse(data: &'a [u8], base: usize) -> Option<Self> {
let header_end = base.checked_add(SUPERBLOB_HEADER_SIZE)?;
let _ = data.get(base..header_end)?;
let magic = read_u32_be_at(data, base)?;
if magic != CSMAGIC_EMBEDDED_SIGNATURE && magic != CSMAGIC_EMBEDDED_SIGNATURE_OLD {
return None;
}
let len_off = base.checked_add(4)?;
let count_off = base.checked_add(8)?;
let length = read_u32_be_at(data, len_off)?;
let count = read_u32_be_at(data, count_off)?;
let need =
SUPERBLOB_HEADER_SIZE.checked_add((count as usize).checked_mul(BLOB_INDEX_SIZE)?)?;
if (length as usize) < need {
return None;
}
let blob_end = base.checked_add(length as usize)?;
if blob_end > data.len() {
return None;
}
Some(Self {
data,
base,
magic,
length,
count,
})
}
pub fn magic(&self) -> u32 {
self.magic
}
pub fn length(&self) -> u32 {
self.length
}
pub fn blob_count(&self) -> u32 {
self.count
}
pub fn blobs(&self) -> BlobIter<'a> {
BlobIter {
data: self.data,
base: self.base,
count: self.count,
cursor: 0,
}
}
#[allow(dead_code)] pub(crate) fn find_blob_bytes(&self, target: Slot) -> Option<&'a [u8]> {
for idx in self.blobs() {
if idx.slot == target {
return self.blob_bytes_at(idx.offset);
}
}
None
}
#[allow(dead_code)] pub(crate) fn blob_bytes_at(&self, offset: u32) -> Option<&'a [u8]> {
let abs = self.base.checked_add(offset as usize)?;
let header = self.data.get(abs..abs.checked_add(BLOB_HEADER_SIZE)?)?;
let blob_len = read_u32_be_at(header, 4)? as usize;
if blob_len < BLOB_HEADER_SIZE {
return None;
}
let end = abs.checked_add(blob_len)?;
self.data.get(abs..end)
}
}
pub mod cd_version {
pub const BASE: u32 = 0x0002_0001;
pub const SUPPORTS_SCATTER: u32 = 0x0002_0100;
pub const SUPPORTS_TEAM_ID: u32 = 0x0002_0200;
pub const SUPPORTS_CODE_LIMIT64: u32 = 0x0002_0300;
pub const SUPPORTS_EXEC_SEG: u32 = 0x0002_0400;
pub const SUPPORTS_LINKAGE: u32 = 0x0002_0500;
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CdFlags: u32 {
const VALID = 0x1;
const ADHOC = 0x2;
const GET_TASK_ALLOW = 0x4;
const INSTALLER = 0x8;
const FORCED_LV = 0x10;
const INVALID_ALLOWED = 0x20;
const HARD = 0x100;
const KILL = 0x200;
const CHECK_EXPIRATION = 0x400;
const RESTRICT = 0x800;
const ENFORCEMENT = 0x1000;
const REQUIRE_LV = 0x2000;
const ENTITLEMENTS_VALIDATED = 0x4000;
const NVRAM_UNRESTRICTED = 0x8000;
const RUNTIME = 0x10000;
const LINKER_SIGNED = 0x20000;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashType {
Sha1,
Sha256,
Sha256Truncated,
Sha384,
Sha512,
Other(u8),
}
impl HashType {
#[inline]
pub fn from_raw(raw: u8) -> Self {
match raw {
1 => Self::Sha1,
2 => Self::Sha256,
3 => Self::Sha256Truncated,
4 => Self::Sha384,
5 => Self::Sha512,
other => Self::Other(other),
}
}
#[inline]
pub fn digest_size(self) -> Option<usize> {
match self {
Self::Sha1 => Some(20),
Self::Sha256 => Some(32),
Self::Sha256Truncated => Some(20),
Self::Sha384 => Some(48),
Self::Sha512 => Some(64),
Self::Other(_) => None,
}
}
}
const CD_FIELD_VERSION: usize = 8;
const CD_FIELD_FLAGS: usize = 12;
const CD_FIELD_HASH_OFFSET: usize = 16;
const CD_FIELD_IDENT_OFFSET: usize = 20;
const CD_FIELD_N_SPECIAL_SLOTS: usize = 24;
const CD_FIELD_N_CODE_SLOTS: usize = 28;
const CD_FIELD_CODE_LIMIT: usize = 32;
const CD_FIELD_HASH_SIZE: usize = 36;
const CD_FIELD_HASH_TYPE: usize = 37;
const CD_FIELD_PLATFORM: usize = 38;
const CD_FIELD_PAGE_SIZE: usize = 39;
const CD_FIELD_TEAM_OFFSET: usize = 48;
const CD_FIELD_CODE_LIMIT_64: usize = 56;
const CD_FIELD_EXEC_SEG_BASE: usize = 64;
const CD_FIELD_EXEC_SEG_LIMIT: usize = 72;
const CD_FIELD_EXEC_SEG_FLAGS: usize = 80;
#[derive(Debug, Clone, Copy)]
pub struct CodeDirectory<'a> {
blob: &'a [u8],
version: u32,
flags: u32,
hash_offset: u32,
ident_offset: u32,
n_special_slots: u32,
n_code_slots: u32,
code_limit: u32,
hash_size: u8,
hash_type_raw: u8,
platform: u8,
page_size_log2: u8,
team_offset: u32,
}
impl<'a> CodeDirectory<'a> {
pub fn parse(blob: &'a [u8]) -> Option<Self> {
if blob.len() < CD_FIELD_PAGE_SIZE.checked_add(1)? {
return None;
}
let magic = read_u32_be_at(blob, 0)?;
if magic != CSMAGIC_CODEDIRECTORY {
return None;
}
let version = read_u32_be_at(blob, CD_FIELD_VERSION)?;
let flags = read_u32_be_at(blob, CD_FIELD_FLAGS)?;
let hash_offset = read_u32_be_at(blob, CD_FIELD_HASH_OFFSET)?;
let ident_offset = read_u32_be_at(blob, CD_FIELD_IDENT_OFFSET)?;
let n_special_slots = read_u32_be_at(blob, CD_FIELD_N_SPECIAL_SLOTS)?;
let n_code_slots = read_u32_be_at(blob, CD_FIELD_N_CODE_SLOTS)?;
let code_limit = read_u32_be_at(blob, CD_FIELD_CODE_LIMIT)?;
let hash_size = *blob.get(CD_FIELD_HASH_SIZE)?;
let hash_type_raw = *blob.get(CD_FIELD_HASH_TYPE)?;
let platform = *blob.get(CD_FIELD_PLATFORM)?;
let page_size_log2 = *blob.get(CD_FIELD_PAGE_SIZE)?;
let team_offset = if version >= cd_version::SUPPORTS_TEAM_ID {
read_u32_be_at(blob, CD_FIELD_TEAM_OFFSET).unwrap_or(0)
} else {
0
};
Some(Self {
blob,
version,
flags,
hash_offset,
ident_offset,
n_special_slots,
n_code_slots,
code_limit,
hash_size,
hash_type_raw,
platform,
page_size_log2,
team_offset,
})
}
pub fn version(&self) -> u32 {
self.version
}
pub fn flags(&self) -> CdFlags {
CdFlags::from_bits_retain(self.flags)
}
pub fn raw_flags(&self) -> u32 {
self.flags
}
pub fn hash_type(&self) -> HashType {
HashType::from_raw(self.hash_type_raw)
}
pub fn hash_size(&self) -> u8 {
self.hash_size
}
pub fn page_size(&self) -> u32 {
if self.page_size_log2 == 0 {
0
} else {
1u32.wrapping_shl(self.page_size_log2 as u32)
}
}
pub fn n_special_slots(&self) -> u32 {
self.n_special_slots
}
pub fn n_code_slots(&self) -> u32 {
self.n_code_slots
}
pub fn code_limit(&self) -> u32 {
self.code_limit
}
pub fn platform(&self) -> u8 {
self.platform
}
pub fn identifier(&self) -> Option<&'a str> {
if self.ident_offset == 0 {
return None;
}
read_cstr_at(self.blob, self.ident_offset as usize)
}
pub fn team_id(&self) -> Option<&'a str> {
if self.version < cd_version::SUPPORTS_TEAM_ID || self.team_offset == 0 {
return None;
}
read_cstr_at(self.blob, self.team_offset as usize)
}
pub fn cd_hash(&self) -> Vec<u8> {
match self.hash_type() {
HashType::Sha256 => Sha256::digest(self.blob).to_vec(),
HashType::Sha384 => Sha384::digest(self.blob).to_vec(),
HashType::Sha512 => Sha512::digest(self.blob).to_vec(),
HashType::Sha1 | HashType::Sha256Truncated | HashType::Other(_) => Vec::new(),
}
}
pub fn cd_hash_truncated(&self) -> [u8; 20] {
let mut out = [0u8; 20];
match self.hash_type() {
HashType::Sha256 => copy_prefix(&mut out, &Sha256::digest(self.blob)),
HashType::Sha384 => copy_prefix(&mut out, &Sha384::digest(self.blob)),
HashType::Sha512 => copy_prefix(&mut out, &Sha512::digest(self.blob)),
HashType::Sha1 | HashType::Sha256Truncated | HashType::Other(_) => {}
}
out
}
pub fn blob_bytes(&self) -> &'a [u8] {
self.blob
}
pub fn code_limit_64(&self) -> Option<u64> {
if self.version < cd_version::SUPPORTS_CODE_LIMIT64 {
return None;
}
read_u64_be_at(self.blob, CD_FIELD_CODE_LIMIT_64)
}
pub fn exec_seg_base(&self) -> Option<u64> {
if self.version < cd_version::SUPPORTS_EXEC_SEG {
return None;
}
read_u64_be_at(self.blob, CD_FIELD_EXEC_SEG_BASE)
}
pub fn exec_seg_limit(&self) -> Option<u64> {
if self.version < cd_version::SUPPORTS_EXEC_SEG {
return None;
}
read_u64_be_at(self.blob, CD_FIELD_EXEC_SEG_LIMIT)
}
pub fn exec_seg_flags(&self) -> Option<u64> {
if self.version < cd_version::SUPPORTS_EXEC_SEG {
return None;
}
read_u64_be_at(self.blob, CD_FIELD_EXEC_SEG_FLAGS)
}
pub fn special_hashes(&self) -> SpecialHashIter<'a> {
SpecialHashIter {
blob: self.blob,
hash_offset: self.hash_offset,
hash_size: self.hash_size,
n_special: self.n_special_slots,
cursor: 0,
}
}
pub fn code_hashes(&self) -> CodeHashIter<'a> {
CodeHashIter {
blob: self.blob,
hash_offset: self.hash_offset,
hash_size: self.hash_size,
n_code: self.n_code_slots,
cursor: 0,
}
}
}
pub struct SpecialHashIter<'a> {
blob: &'a [u8],
hash_offset: u32,
hash_size: u8,
n_special: u32,
cursor: u32,
}
impl<'a> Iterator for SpecialHashIter<'a> {
type Item = (i32, &'a [u8]);
fn next(&mut self) -> Option<Self::Item> {
if self.cursor >= self.n_special {
return None;
}
let i = self.cursor.checked_add(1)?;
self.cursor = i;
let off_back = (i as usize).checked_mul(self.hash_size as usize)?;
let off = (self.hash_offset as usize).checked_sub(off_back)?;
let end = off.checked_add(self.hash_size as usize)?;
let bytes = self.blob.get(off..end)?;
let slot_idx = i32::try_from(i).ok()?.wrapping_neg();
Some((slot_idx, bytes))
}
}
pub struct CodeHashIter<'a> {
blob: &'a [u8],
hash_offset: u32,
hash_size: u8,
n_code: u32,
cursor: u32,
}
impl<'a> Iterator for CodeHashIter<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<Self::Item> {
if self.cursor >= self.n_code {
return None;
}
let i = self.cursor;
self.cursor = self.cursor.checked_add(1)?;
let off_fwd = (i as usize).checked_mul(self.hash_size as usize)?;
let off = (self.hash_offset as usize).checked_add(off_fwd)?;
let end = off.checked_add(self.hash_size as usize)?;
self.blob.get(off..end)
}
}
impl<'a> Signature<'a> {
pub fn primary_code_directory(&self) -> Option<CodeDirectory<'a>> {
let bytes = self.find_blob_bytes(Slot::CodeDirectory)?;
CodeDirectory::parse(bytes)
}
pub fn alternate_code_directories(&self) -> CodeDirectoryIter<'a> {
CodeDirectoryIter {
sig: *self,
cursor: 0,
}
}
pub fn entitlements(&self) -> Option<Entitlements<'a>> {
let blob = self.find_blob_bytes(Slot::Entitlements)?;
Entitlements::parse(blob)
}
pub fn der_entitlements(&self) -> Option<DerEntitlements<'a>> {
let blob = self.find_blob_bytes(Slot::DerEntitlements)?;
DerEntitlements::parse(blob)
}
pub fn cms(&self) -> Option<CmsSignature<'a>> {
let blob = self.find_blob_bytes(Slot::SignatureSlot)?;
CmsSignature::parse(blob)
}
pub fn requirements(&self) -> Option<Requirements<'a>> {
let blob = self.find_blob_bytes(Slot::Requirements)?;
Requirements::parse(blob)
}
}
pub struct CodeDirectoryIter<'a> {
sig: Signature<'a>,
cursor: u32,
}
impl<'a> Iterator for CodeDirectoryIter<'a> {
type Item = CodeDirectory<'a>;
fn next(&mut self) -> Option<CodeDirectory<'a>> {
while self.cursor < 5 {
let i = self.cursor;
self.cursor = self.cursor.checked_add(1)?;
let target = Slot::AlternateCodeDirectory(i);
if let Some(blob) = self.sig.find_blob_bytes(target)
&& let Some(cd) = CodeDirectory::parse(blob)
{
return Some(cd);
}
}
None
}
}
#[derive(Debug, Clone, Copy)]
pub struct Entitlements<'a> {
payload: &'a [u8],
}
impl<'a> Entitlements<'a> {
pub fn parse(blob: &'a [u8]) -> Option<Self> {
if blob.len() < BLOB_HEADER_SIZE {
return None;
}
let magic = read_u32_be_at(blob, 0)?;
if magic != CSMAGIC_EMBEDDED_ENTITLEMENTS {
return None;
}
let len = read_u32_be_at(blob, 4)? as usize;
if len < BLOB_HEADER_SIZE || len > blob.len() {
return None;
}
let payload = blob.get(BLOB_HEADER_SIZE..len)?;
Some(Self { payload })
}
pub fn raw(&self) -> &'a [u8] {
self.payload
}
pub fn parsed(&self) -> Option<plist::Value> {
plist::Value::from_reader(std::io::Cursor::new(self.payload)).ok()
}
}
#[derive(Debug, Clone, Copy)]
pub struct DerEntitlements<'a> {
payload: &'a [u8],
}
impl<'a> DerEntitlements<'a> {
pub fn parse(blob: &'a [u8]) -> Option<Self> {
if blob.len() < BLOB_HEADER_SIZE {
return None;
}
let magic = read_u32_be_at(blob, 0)?;
if magic != CSMAGIC_EMBEDDED_DER_ENTITLEMENTS {
return None;
}
let len = read_u32_be_at(blob, 4)? as usize;
if len < BLOB_HEADER_SIZE || len > blob.len() {
return None;
}
let payload = blob.get(BLOB_HEADER_SIZE..len)?;
Some(Self { payload })
}
pub fn raw(&self) -> &'a [u8] {
self.payload
}
pub fn keys(&self) -> Vec<String> {
let mut out = der_collect_keys(self.payload);
out.sort();
out.dedup();
out
}
pub fn pairs(&self) -> Vec<(String, DerEntitlementValue)> {
der_collect_pairs(self.payload)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DerEntitlementValue {
Bool(bool),
Integer(i64),
String(String),
Array(Vec<DerEntitlementValue>),
Other(u8),
}
impl DerEntitlementValue {
#[must_use]
pub fn kind_label(&self) -> &'static str {
match self {
Self::Bool(_) => "bool",
Self::Integer(_) => "integer",
Self::String(_) => "string",
Self::Array(_) => "array",
Self::Other(_) => "data",
}
}
#[must_use]
pub fn display(&self) -> String {
match self {
Self::Bool(b) => b.to_string(),
Self::Integer(i) => i.to_string(),
Self::String(s) => s.clone(),
Self::Array(items) => {
let inner: Vec<String> = items.iter().map(Self::display).collect();
format!("[{}]", inner.join(", "))
}
Self::Other(tag) => format!("<der tag 0x{tag:02x}>"),
}
}
}
const MAX_DER_ARRAY_ITEMS: usize = 1024;
const MAX_DER_VALUE_DEPTH: u32 = 32;
#[derive(Debug, Clone, Copy)]
pub struct Requirements<'a> {
blob: &'a [u8],
count: u32,
}
impl<'a> Requirements<'a> {
pub fn parse(blob: &'a [u8]) -> Option<Self> {
if blob.len() < BLOB_HEADER_SIZE {
return None;
}
let magic = read_u32_be_at(blob, 0)?;
if magic != CSMAGIC_REQUIREMENTS {
return None;
}
let len = read_u32_be_at(blob, 4)? as usize;
if len < BLOB_HEADER_SIZE || len > blob.len() {
return None;
}
let count = if len >= 12 {
read_u32_be_at(blob, 8).unwrap_or(0)
} else {
0
};
Some(Self {
blob: blob.get(..len)?,
count,
})
}
pub fn raw(&self) -> &'a [u8] {
self.blob
}
pub fn count(&self) -> u32 {
self.count
}
pub fn len(&self) -> usize {
self.blob.len()
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn decoded(&self) -> Vec<DecodedRequirement> {
let mut out = Vec::new();
let count = core::cmp::min(self.count as usize, MAX_REQUIREMENT_ENTRIES);
for i in 0..count {
let Some(entry_off) =
REQUIREMENTS_INDEX_OFFSET.checked_add(i.saturating_mul(BLOB_INDEX_SIZE))
else {
break;
};
let (Some(type_raw), Some(blob_off)) = (
read_u32_be_at(self.blob, entry_off),
read_u32_be_at(self.blob, entry_off.saturating_add(4)),
) else {
break;
};
let kind = RequirementKind::from_raw(type_raw);
if let Some(text) = decode_requirement_blob(self.blob, blob_off as usize) {
out.push(DecodedRequirement { kind, text });
}
}
out
}
}
const REQUIREMENTS_INDEX_OFFSET: usize = 12;
const MAX_REQUIREMENT_ENTRIES: usize = 32;
const MAX_REQUIREMENT_DEPTH: u32 = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequirementKind {
Host,
Guest,
Designated,
Library,
Plugin,
Other(u32),
}
impl RequirementKind {
#[must_use]
pub fn from_raw(raw: u32) -> Self {
match raw {
1 => Self::Host,
2 => Self::Guest,
3 => Self::Designated,
4 => Self::Library,
5 => Self::Plugin,
other => Self::Other(other),
}
}
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::Host => "host",
Self::Guest => "guest",
Self::Designated => "designated",
Self::Library => "library",
Self::Plugin => "plugin",
Self::Other(_) => "requirement",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedRequirement {
pub kind: RequirementKind,
pub text: String,
}
#[derive(Debug, Clone, Copy)]
pub struct CmsSignature<'a> {
payload: &'a [u8],
}
impl<'a> CmsSignature<'a> {
pub fn parse(blob: &'a [u8]) -> Option<Self> {
if blob.len() < BLOB_HEADER_SIZE {
return None;
}
let magic = read_u32_be_at(blob, 0)?;
if magic != CSMAGIC_BLOBWRAPPER {
return None;
}
let len = read_u32_be_at(blob, 4)? as usize;
if len < BLOB_HEADER_SIZE || len > blob.len() {
return None;
}
let payload = blob.get(BLOB_HEADER_SIZE..len)?;
Some(Self { payload })
}
pub fn raw(&self) -> &'a [u8] {
self.payload
}
pub fn is_present(&self) -> bool {
!self.payload.is_empty()
}
pub fn len(&self) -> usize {
self.payload.len()
}
pub fn is_empty(&self) -> bool {
self.payload.is_empty()
}
}
fn der_collect_keys(payload: &[u8]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let entries = der_locate_entries(payload).unwrap_or(payload);
let mut cursor = 0usize;
while cursor < entries.len() {
let Some(remaining) = entries.get(cursor..) else {
break;
};
let Some(hdr) = der_read_header(remaining) else {
break;
};
let body_start = match cursor.checked_add(hdr.header_len) {
Some(v) => v,
None => break,
};
let body_end = match body_start.checked_add(hdr.length) {
Some(v) => v,
None => break,
};
if body_end > entries.len() {
break;
}
if hdr.tag == 0x30
&& let Some(seq_body) = entries.get(body_start..body_end)
&& let Some(key) = der_first_utf8_string(seq_body)
{
out.push(key);
}
cursor = body_end;
}
out
}
fn der_locate_entries(payload: &[u8]) -> Option<&[u8]> {
let outer = der_read_header(payload)?;
let outer_body_start = outer.header_len;
let outer_body_end = outer_body_start.checked_add(outer.length)?;
let outer_body = payload.get(outer_body_start..outer_body_end)?;
let mut cursor = 0usize;
let inner = loop {
let remaining = outer_body.get(cursor..)?;
if remaining.is_empty() {
return None;
}
let hdr = der_read_header(remaining)?;
let body_start = cursor.checked_add(hdr.header_len)?;
let body_end = body_start.checked_add(hdr.length)?;
if hdr.tag == 0x02 {
cursor = body_end;
continue;
}
if matches!(hdr.tag, 0x30 | 0x31 | 0xa0 | 0xb0) {
break outer_body.get(body_start..body_end)?;
}
return None;
};
Some(inner)
}
fn der_collect_pairs(payload: &[u8]) -> Vec<(String, DerEntitlementValue)> {
let mut out: Vec<(String, DerEntitlementValue)> = Vec::new();
let entries = der_locate_entries(payload).unwrap_or(payload);
let mut cursor = 0usize;
while cursor < entries.len() {
let Some(remaining) = entries.get(cursor..) else {
break;
};
let Some(hdr) = der_read_header(remaining) else {
break;
};
let Some(body_start) = cursor.checked_add(hdr.header_len) else {
break;
};
let Some(body_end) = body_start.checked_add(hdr.length) else {
break;
};
if body_end > entries.len() {
break;
}
if hdr.tag == 0x30
&& let Some(seq_body) = entries.get(body_start..body_end)
&& let Some(pair) = der_entry_pair(seq_body)
{
out.push(pair);
}
cursor = body_end;
}
out
}
fn der_entry_pair(seq_body: &[u8]) -> Option<(String, DerEntitlementValue)> {
let khdr = der_read_header(seq_body)?;
if khdr.tag != 0x0c {
return None;
}
let kstart = khdr.header_len;
let kend = kstart.checked_add(khdr.length)?;
let kbytes = seq_body.get(kstart..kend)?;
let key = core::str::from_utf8(kbytes).ok()?.to_string();
let value = seq_body
.get(kend..)
.and_then(|rest| der_decode_value(rest, 0))
.unwrap_or(DerEntitlementValue::Other(0));
Some((key, value))
}
fn der_decode_value(buf: &[u8], depth: u32) -> Option<DerEntitlementValue> {
if depth > MAX_DER_VALUE_DEPTH {
return Some(DerEntitlementValue::Other(0));
}
let hdr = der_read_header(buf)?;
let start = hdr.header_len;
let end = start.checked_add(hdr.length)?;
let body = buf.get(start..end)?;
let value = match hdr.tag {
0x01 => DerEntitlementValue::Bool(body.first().copied().unwrap_or(0) != 0),
0x02 => DerEntitlementValue::Integer(der_decode_integer(body)),
0x0c => DerEntitlementValue::String(String::from_utf8_lossy(body).into_owned()),
0x30 | 0x31 | 0xa0 | 0xb0 => {
let mut items: Vec<DerEntitlementValue> = Vec::new();
let mut cursor = 0usize;
while cursor < body.len() && items.len() < MAX_DER_ARRAY_ITEMS {
let Some(rem) = body.get(cursor..) else {
break;
};
let Some(h) = der_read_header(rem) else {
break;
};
let Some(bs) = cursor.checked_add(h.header_len) else {
break;
};
let Some(be) = bs.checked_add(h.length) else {
break;
};
if be > body.len() {
break;
}
if let Some(item) = body.get(cursor..be).and_then(|tlv| {
der_decode_value(tlv, depth.checked_add(1).unwrap_or(MAX_DER_VALUE_DEPTH))
}) {
items.push(item);
}
cursor = be;
}
DerEntitlementValue::Array(items)
}
other => DerEntitlementValue::Other(other),
};
Some(value)
}
fn der_decode_integer(body: &[u8]) -> i64 {
let mut v: i64 = 0;
for b in body.iter().take(8) {
v = v.wrapping_shl(8) | i64::from(*b);
}
v
}
fn der_first_utf8_string(seq_body: &[u8]) -> Option<String> {
let hdr = der_read_header(seq_body)?;
if hdr.tag != 0x0c {
return None;
}
let body_start = hdr.header_len;
let body_end = body_start.checked_add(hdr.length)?;
let body = seq_body.get(body_start..body_end)?;
core::str::from_utf8(body).ok().map(String::from)
}
fn copy_prefix(dst: &mut [u8], src: &[u8]) {
let n = core::cmp::min(dst.len(), src.len());
if let (Some(dst_head), Some(src_head)) = (dst.get_mut(..n), src.get(..n)) {
dst_head.copy_from_slice(src_head);
}
}
struct DerHeader {
tag: u8,
length: usize,
header_len: usize,
}
fn der_read_header(buf: &[u8]) -> Option<DerHeader> {
let tag = *buf.first()?;
let len_byte = *buf.get(1)?;
if len_byte < 0x80 {
return Some(DerHeader {
tag,
length: len_byte as usize,
header_len: 2,
});
}
let n = (len_byte & 0x7f) as usize;
if n == 0 || n > 4 {
return None;
}
let mut length = 0usize;
for i in 0..n {
let b = *buf.get(2usize.checked_add(i)?)?;
length = length.wrapping_shl(8) | (b as usize);
}
Some(DerHeader {
tag,
length,
header_len: 2usize.checked_add(n)?,
})
}
mod req_op {
pub const FALSE: u32 = 0;
pub const TRUE: u32 = 1;
pub const IDENT: u32 = 2;
pub const APPLE_ANCHOR: u32 = 3;
pub const ANCHOR_HASH: u32 = 4;
pub const INFO_KEY_VALUE: u32 = 5;
pub const AND: u32 = 6;
pub const OR: u32 = 7;
pub const CD_HASH: u32 = 8;
pub const NOT: u32 = 9;
pub const INFO_KEY_FIELD: u32 = 10;
pub const CERT_FIELD: u32 = 11;
pub const TRUSTED_CERT: u32 = 12;
pub const TRUSTED_CERTS: u32 = 13;
pub const CERT_GENERIC: u32 = 14;
pub const APPLE_GENERIC_ANCHOR: u32 = 15;
pub const ENTITLEMENT_FIELD: u32 = 16;
pub const CERT_POLICY: u32 = 17;
pub const NAMED_ANCHOR: u32 = 18;
pub const NAMED_CODE: u32 = 19;
}
const REQ_OP_FLAG_MASK: u32 = 0xFF00_0000;
const REQ_OP_GENERIC_FALSE: u32 = 0x8000_0000;
const REQ_OP_GENERIC_SKIP: u32 = 0x4000_0000;
mod match_op {
pub const EXISTS: u32 = 0;
pub const EQUAL: u32 = 1;
pub const CONTAINS: u32 = 2;
pub const BEGINS_WITH: u32 = 3;
pub const ENDS_WITH: u32 = 4;
pub const LESS_THAN: u32 = 5;
pub const GREATER_THAN: u32 = 6;
pub const LESS_EQUAL: u32 = 7;
pub const GREATER_EQUAL: u32 = 8;
}
const SL_PRIMARY: u32 = 0;
const SL_AND: u32 = 1;
const SL_OR: u32 = 2;
const SL_TOP: u32 = 3;
struct ReqCursor<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> ReqCursor<'a> {
fn new(buf: &'a [u8]) -> Self {
Self { buf, pos: 0 }
}
fn u32(&mut self) -> Option<u32> {
let v = read_u32_be_at(self.buf, self.pos)?;
self.pos = self.pos.checked_add(4)?;
Some(v)
}
fn data(&mut self) -> Option<&'a [u8]> {
let len = self.u32()? as usize;
let end = self.pos.checked_add(len)?;
let slice = self.buf.get(self.pos..end)?;
let aligned = len.checked_add(3)? & !3usize;
self.pos = self.pos.checked_add(aligned)?;
Some(slice)
}
}
fn decode_requirement_blob(vector: &[u8], off: usize) -> Option<String> {
let magic = read_u32_be_at(vector, off)?;
if magic != CSMAGIC_REQUIREMENT {
return None;
}
let length = read_u32_be_at(vector, off.checked_add(4)?)? as usize;
let blob_end = off.checked_add(length)?;
let expr_start = off.checked_add(12)?;
let expr = vector.get(expr_start..blob_end)?;
let mut cursor = ReqCursor::new(expr);
eval_expression(&mut cursor, SL_TOP, 0)
}
fn eval_expression(cursor: &mut ReqCursor<'_>, syntax_level: u32, depth: u32) -> Option<String> {
if depth > MAX_REQUIREMENT_DEPTH {
return Some("/* … */".to_string());
}
let next = depth.checked_add(1)?;
let op = cursor.u32()?;
match op {
req_op::FALSE => Some("never".to_string()),
req_op::TRUE => Some("always".to_string()),
req_op::IDENT => Some(format!("identifier \"{}\"", utf8_lossy(cursor.data()?))),
req_op::APPLE_ANCHOR => Some("anchor apple".to_string()),
req_op::APPLE_GENERIC_ANCHOR => Some("anchor apple generic".to_string()),
req_op::ANCHOR_HASH => {
let slot = get_cert_slot(cursor)?;
let data = cursor.data()?;
Some(format!("certificate {slot} = H\"{}\"", hex_encode(data)))
}
req_op::INFO_KEY_VALUE => {
let key = utf8_lossy(cursor.data()?);
let value = utf8_lossy(cursor.data()?);
Some(format!("info[{key}] = \"{value}\""))
}
req_op::AND => {
let paren = syntax_level < SL_AND;
let left = eval_expression(cursor, SL_AND, next)?;
let right = eval_expression(cursor, SL_AND, next)?;
Some(wrap(paren, format!("{left} and {right}")))
}
req_op::OR => {
let paren = syntax_level < SL_OR;
let left = eval_expression(cursor, SL_OR, next)?;
let right = eval_expression(cursor, SL_OR, next)?;
Some(wrap(paren, format!("{left} or {right}")))
}
req_op::NOT => Some(format!("! {}", eval_expression(cursor, SL_PRIMARY, next)?)),
req_op::CD_HASH => Some(format!("cdhash H\"{}\"", hex_encode(cursor.data()?))),
req_op::INFO_KEY_FIELD => {
let key = utf8_lossy(cursor.data()?);
let m = get_match(cursor)?;
Some(format!("info[{key}]{m}"))
}
req_op::ENTITLEMENT_FIELD => {
let key = utf8_lossy(cursor.data()?);
let m = get_match(cursor)?;
Some(format!("entitlement[{key}]{m}"))
}
req_op::CERT_FIELD => {
let slot = get_cert_slot(cursor)?;
let field = utf8_lossy(cursor.data()?);
let m = get_match(cursor)?;
Some(format!("certificate {slot}[{field}]{m}"))
}
req_op::CERT_GENERIC => {
let slot = get_cert_slot(cursor)?;
let oid = decode_oid(cursor.data()?);
let m = get_match(cursor)?;
Some(format!("certificate {slot}[field.{oid}]{m}"))
}
req_op::CERT_POLICY => {
let slot = get_cert_slot(cursor)?;
let oid = decode_oid(cursor.data()?);
let m = get_match(cursor)?;
Some(format!("certificate {slot}[policy.{oid}]{m}"))
}
req_op::TRUSTED_CERT => Some(format!("certificate {} trusted", get_cert_slot(cursor)?)),
req_op::TRUSTED_CERTS => Some("anchor trusted".to_string()),
req_op::NAMED_ANCHOR => Some(format!("anchor apple {}", utf8_lossy(cursor.data()?))),
req_op::NAMED_CODE => Some(format!("({})", utf8_lossy(cursor.data()?))),
other => {
if other & REQ_OP_GENERIC_FALSE != 0 {
Some(format!("false /* opcode {} */", other & !REQ_OP_FLAG_MASK))
} else if other & REQ_OP_GENERIC_SKIP != 0 {
Some(format!("/* opcode {} */", other & !REQ_OP_FLAG_MASK))
} else {
Some(format!("/* unknown opcode {other} */"))
}
}
}
}
fn get_match(cursor: &mut ReqCursor<'_>) -> Option<String> {
let op = cursor.u32()?;
let text = match op {
match_op::EXISTS => " /* exists */".to_string(),
match_op::EQUAL => format!(" = \"{}\"", utf8_lossy(cursor.data()?)),
match_op::CONTAINS => format!(" ~ \"{}\"", utf8_lossy(cursor.data()?)),
match_op::BEGINS_WITH => format!(" = \"{}*\"", utf8_lossy(cursor.data()?)),
match_op::ENDS_WITH => format!(" = \"*{}\"", utf8_lossy(cursor.data()?)),
match_op::LESS_THAN => format!(" < \"{}\"", utf8_lossy(cursor.data()?)),
match_op::GREATER_THAN => format!(" > \"{}\"", utf8_lossy(cursor.data()?)),
match_op::LESS_EQUAL => format!(" <= \"{}\"", utf8_lossy(cursor.data()?)),
match_op::GREATER_EQUAL => format!(" >= \"{}\"", utf8_lossy(cursor.data()?)),
_ => " /* match? */".to_string(),
};
Some(text)
}
fn get_cert_slot(cursor: &mut ReqCursor<'_>) -> Option<String> {
let slot = cursor.u32()? as i32;
Some(match slot {
0 => "leaf".to_string(),
-1 => "root".to_string(),
n => n.to_string(),
})
}
fn decode_oid(data: &[u8]) -> String {
let mut pos = 0usize;
let Some(first) = oid_component(data, &mut pos) else {
return String::new();
};
let q1 = core::cmp::min(first.checked_div(40).unwrap_or(0), 2);
let second = first.saturating_sub(q1.saturating_mul(40));
let mut out = format!("{q1}.{second}");
while let Some(v) = oid_component(data, &mut pos) {
out.push('.');
out.push_str(&v.to_string());
}
out
}
fn oid_component(data: &[u8], pos: &mut usize) -> Option<u32> {
let mut result: u32 = 0;
loop {
let b = *data.get(*pos)?;
*pos = pos.checked_add(1)?;
result = result.wrapping_mul(128).wrapping_add((b & 0x7f) as u32);
if b & 0x80 == 0 {
break;
}
}
Some(result)
}
fn wrap(paren: bool, s: String) -> String {
if paren { format!("({s})") } else { s }
}
fn utf8_lossy(data: &[u8]) -> String {
String::from_utf8_lossy(data).into_owned()
}
fn hex_encode(data: &[u8]) -> String {
use core::fmt::Write as _;
let mut out = String::with_capacity(data.len().saturating_mul(2));
for b in data {
let _ = write!(out, "{b:02x}");
}
out
}
pub struct BlobIter<'a> {
data: &'a [u8],
base: usize,
count: u32,
cursor: u32,
}
impl<'a> Iterator for BlobIter<'a> {
type Item = BlobIndex;
fn next(&mut self) -> Option<BlobIndex> {
if self.cursor >= self.count {
return None;
}
let i = self.cursor;
self.cursor = self.cursor.checked_add(1)?;
let entry_off = self
.base
.checked_add(SUPERBLOB_HEADER_SIZE)?
.checked_add((i as usize).checked_mul(BLOB_INDEX_SIZE)?)?;
let raw = read_u32_be_at(self.data, entry_off)?;
let off_off = entry_off.checked_add(4)?;
let offset = read_u32_be_at(self.data, off_off)?;
Some(BlobIndex {
slot: Slot::from_raw(raw),
raw_slot: raw,
offset,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slot_round_trip_known_values() {
for (raw, want) in [
(0u32, Slot::CodeDirectory),
(1, Slot::InfoSlot),
(2, Slot::Requirements),
(3, Slot::ResourceDir),
(4, Slot::Application),
(5, Slot::Entitlements),
(7, Slot::DerEntitlements),
(8, Slot::LaunchConstraintSelf),
(9, Slot::LaunchConstraintParent),
(10, Slot::LaunchConstraintResponsible),
(11, Slot::LibraryConstraint),
(0x1000, Slot::AlternateCodeDirectory(0)),
(0x1004, Slot::AlternateCodeDirectory(4)),
(0x10000, Slot::SignatureSlot),
(0x10001, Slot::IdentificationSlot),
(0x10002, Slot::TicketSlot),
] {
assert_eq!(Slot::from_raw(raw), want);
}
assert_eq!(Slot::from_raw(99), Slot::Other(99));
assert_eq!(Slot::from_raw(0x1005), Slot::Other(0x1005));
}
#[test]
fn signature_parse_rejects_wrong_magic() {
let bytes = [
0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, ];
assert!(Signature::parse(&bytes, 0).is_none());
}
#[test]
fn signature_parse_accepts_embedded_magic() {
let bytes = [
0xfa, 0xde, 0x0c, 0xc0, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, ];
let sig = Signature::parse(&bytes, 0).unwrap();
assert_eq!(sig.magic(), CSMAGIC_EMBEDDED_SIGNATURE);
assert_eq!(sig.length(), 12);
assert_eq!(sig.blob_count(), 0);
assert_eq!(sig.blobs().count(), 0);
}
#[test]
fn signature_blob_iter_returns_index_entries() {
let mut bytes = vec![
0xfa, 0xde, 0x0c, 0xc0, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, ];
bytes.extend([0; 8]);
let sig = Signature::parse(&bytes, 0).unwrap();
let blobs: Vec<_> = sig.blobs().collect();
assert_eq!(blobs.len(), 1);
assert_eq!(blobs[0].slot, Slot::Requirements);
assert_eq!(blobs[0].raw_slot, 2);
assert_eq!(blobs[0].offset, 20);
}
#[test]
fn requirements_decode_designated_expression() {
let mut v: Vec<u8> = Vec::new();
v.extend(CSMAGIC_REQUIREMENTS.to_be_bytes()); v.extend(52u32.to_be_bytes()); v.extend(1u32.to_be_bytes()); v.extend(3u32.to_be_bytes()); v.extend(20u32.to_be_bytes()); v.extend(CSMAGIC_REQUIREMENT.to_be_bytes()); v.extend(32u32.to_be_bytes()); v.extend(1u32.to_be_bytes()); v.extend(6u32.to_be_bytes()); v.extend(2u32.to_be_bytes()); v.extend(1u32.to_be_bytes()); v.extend([0x61, 0x00, 0x00, 0x00]); v.extend(15u32.to_be_bytes()); assert_eq!(v.len(), 52);
let reqs = Requirements::parse(&v).expect("parse requirements");
let decoded = reqs.decoded();
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].kind, RequirementKind::Designated);
assert_eq!(decoded[0].text, "identifier \"a\" and anchor apple generic");
}
#[test]
fn der_entitlement_pairs_decode_key_and_value() {
let payload: [u8; 8] = [0x30, 0x06, 0x0c, 0x01, 0x6b, 0x01, 0x01, 0xff];
let pairs = der_collect_pairs(&payload);
assert_eq!(pairs.len(), 1);
assert_eq!(pairs[0].0, "k");
assert_eq!(pairs[0].1, DerEntitlementValue::Bool(true));
assert_eq!(pairs[0].1.kind_label(), "bool");
assert_eq!(pairs[0].1.display(), "true");
}
}