// The embedded code signature: the superblob index and the code directory.
//
// `LC_CODE_SIGNATURE` names a byte range that holds a `SuperBlob`: a count,
// then an index of (type, offset) pairs, then the blobs themselves. The two
// blobs worth decoding without a full CMS implementation are the CodeDirectory,
// which says what was signed and how, and the entitlements, which say what the
// process is allowed to do.
///|
/// `LC_CODE_SIGNATURE`.
pub const LC_CODE_SIGNATURE : UInt32 = 0x1D
///|
/// Magic of the outer `SuperBlob`.
pub const CSMAGIC_EMBEDDED_SIGNATURE : UInt32 = 0xFADE_0CC0
///|
/// Magic of a CodeDirectory blob.
pub const CSMAGIC_CODEDIRECTORY : UInt32 = 0xFADE_0C02
///|
/// The `CSSLOT_ENTITLEMENTS` index type: a DER-encoded entitlement plist.
///
/// The value is 5, but Apple's embedded signatures use slot 0x10000 for it; the
/// two are distinct constants in `` and only the latter
/// appears in binaries produced by the current toolchain.
pub const CSSLOT_ENTITLEMENTS : UInt32 = 0x0001_0000
///|
/// Where the signature lives inside the file.
pub(all) struct SignatureRegion {
/// `dataoff`: absolute offset of the SuperBlob.
offset : Int64
/// `datasize`: its byte length.
size : Int64
}
///|
/// One entry of the SuperBlob index.
pub(all) struct SignatureBlobRef {
/// `CSSLOT_*` type number.
slot : UInt32
/// Offset of the blob from the start of the SuperBlob.
offset : Int64
/// Blob magic, read from the blob header.
magic : UInt32
/// Blob length, read from the blob header.
length : Int64
}
///|
/// A decoded `CodeDirectory`.
///
/// Only the fields that identify the signature are decoded: the version tells a
/// reader how far the structure extends, and hashing the rest would require
/// re-deriving every page hash, which is a verification concern rather than an
/// inspection one.
pub(all) struct CodeDirectory {
/// `CSMAGIC_CODEDIRECTORY`.
magic : UInt32
/// Byte length of the directory.
length : Int64
/// Structure version, e.g. `0x20400`.
version : UInt32
/// `CD_*` flag bits.
flags : UInt32
/// Signing identifier, usually a reverse-DNS bundle id.
identifier : String
/// `teamID`, present from version `0x20200`.
team_id : String
/// `hashType`: 1 SHA-1, 2 SHA-256.
hash_type : UInt32
/// `hashSize`: bytes per hash, 20 for SHA-1 and 32 for SHA-256.
hash_size : UInt32
/// `pageSize`: log2 of the page size the hashes cover, or 0 for one hash.
page_size : UInt32
/// `nCodeSlots`: number of page hashes.
code_slots : UInt32
}
///|
/// Finds the `LC_CODE_SIGNATURE` command in a slice.
pub fn signature_region(
slice : MachSlice,
) -> SignatureRegion? raise MachscopeError {
for cmd in slice.commands {
if cmd.cmd != LC_CODE_SIGNATURE {
continue
}
let body = command_payload(slice, cmd)
let cur = Cursor::{ pos: body.start, endian: slice.header.kind.endian(), }
let (dataoff, cur) = body.read_u32(cur)
let (datasize, _) = body.read_u32(cur)
// `dataoff` is relative to the image, so a fat slice adds its own base.
let region = SignatureRegion::{
offset: slice.bounds.start + dataoff.to_int64(),
size: datasize.to_int64(),
}
slice.bounds.require(region.offset, region.size, "code signature")
return Some(region)
}
None
}
///|
/// Reads the SuperBlob index of a signature region.
///
/// Every blob's own header is read here so that a caller can inspect the index
/// without decoding blobs it does not need, and so that a blob pointing outside
/// the region is reported before anything tries to parse it.
pub fn signature_blobs(
slice : MachSlice,
region : SignatureRegion,
) -> Array[SignatureBlobRef] raise MachscopeError {
let endian = Endian::Big // code signing structures are always big-endian
let b = slice.bounds
let cur = Cursor::{ pos: region.offset, endian, }
let (magic, cur) = b.read_u32(cur)
if magic != CSMAGIC_EMBEDDED_SIGNATURE {
raise BadMagic(magic~)
}
let (length, cur) = b.read_u32(cur)
let (count, cur) = b.read_u32(cur)
if length.to_int64() > region.size {
raise Inconsistent(
what="SuperBlob",
detail="declared length exceeds the signature region",
)
}
b.require(cur.pos, count.to_int64() * 8L, "signature blob index")
let refs : Array[SignatureBlobRef] = []
let mut at = cur.pos
for _ in 0.. region.offset + length.to_int64() {
raise OutOfBounds(
what="signature blob",
offset=blob_at,
size=8L,
limit=region.offset + length.to_int64(),
)
}
let bc = Cursor::{ pos: blob_at, endian, }
let (blob_magic, bc) = b.read_u32(bc)
let (blob_len, _) = b.read_u32(bc)
refs.push(SignatureBlobRef::{
slot,
offset: offset.to_int64(),
magic: blob_magic,
length: blob_len.to_int64(),
})
at = at + 8L
}
refs
}
///|
/// Decodes the `CodeDirectory` blob from the index.
pub fn code_directory(
slice : MachSlice,
region : SignatureRegion,
) -> CodeDirectory? raise MachscopeError {
let refs = signature_blobs(slice, region)
let mut dir : CodeDirectory? = None
for r in refs {
if r.magic != CSMAGIC_CODEDIRECTORY {
continue
}
dir = Some(decode_code_directory(slice, region, r))
}
dir
}
///|
/// Reads the fixed `CodeDirectory` fields that this library exposes.
fn decode_code_directory(
slice : MachSlice,
region : SignatureRegion,
entry : SignatureBlobRef,
) -> CodeDirectory raise MachscopeError {
let b = slice.bounds
let base = region.offset + entry.offset
let endian = Endian::Big
let c = Cursor::{ pos: base, endian, }
let (magic, c) = b.read_u32(c)
let (length, c) = b.read_u32(c)
let (version, c) = b.read_u32(c)
let (flags, c) = b.read_u32(c)
let (hash_offset, c) = b.read_u32(c)
let (identifier_offset, _) = b.read_u32(c)
// The identifier is an offset from the start of the blob.
let identifier = b.fixed_name(
base + identifier_offset.to_int64(),
256L,
"code directory identifier",
)
// Offsets below are relative to the blob start, per
// ``. Note that nSpecialSlots is a 32-bit word, not a
// byte: it is followed by nCodeSlots, codeLimit, then four single-byte
// fields.
let (slots_at, _) = b.read_u32(Cursor::{ pos: base + 24L, endian, })
let (code_slots, code_limit) = b.read_u32(Cursor::{ pos: base + 28L, endian, })
let (hash_size, after_size) = b.read_u8(Cursor::{ pos: base + 36L, endian, })
let (hash_type, after_type) = b.read_u8(after_size)
let (_, after_platform) = b.read_u8(after_type)
let (page_size, _) = b.read_u8(after_platform)
let _ = (slots_at, code_limit)
// teamID is a separate offset field, present from version 0x20200.
let mut team_id = ""
if version >= 0x0002_0200U {
let team_offset_at = base + 48L
let (team_offset, _) = b.read_u32(Cursor::{ pos: team_offset_at, endian, })
if team_offset != 0U {
team_id = b.fixed_name(
base + team_offset.to_int64(),
256L,
"code directory team id",
)
}
}
// `hash_offset` must point inside the blob; checking it here means the page
// hashes are known to be addressable even though this version does not read
// them.
if hash_offset.to_int64() > entry.length {
raise Inconsistent(
what="CodeDirectory",
detail="hash offset outside the directory",
)
}
CodeDirectory::{
magic,
length: length.to_int64(),
version,
flags,
identifier,
team_id,
hash_type,
hash_size,
page_size,
code_slots,
}
}
///|
/// Extracts the entitlements plist from a signature, if there is one.
///
/// The payload is DER-wrapped: a PKCS#7 `SignedData` whose entitlement
/// attribute holds the property list. The returned bytes are the plist itself,
/// not yet parsed.
pub fn entitlements_der(
slice : MachSlice,
region : SignatureRegion,
) -> Bytes? raise MachscopeError {
let refs = signature_blobs(slice, region)
let mut payload : Bytes? = None
for r in refs {
if r.slot != CSSLOT_ENTITLEMENTS {
continue
}
let base = region.offset + r.offset
// A blob is a magic, a length, then the payload.
let blob = slice.bounds.slice(base + 8L, r.length - 8L, "entitlements blob")
payload = Some(entitlements_from_blob(blob))
}
payload
}
///|
/// Walks a PKCS#7 blob down to the octet string holding the plist.
///
/// The layout is `ContentInfo > SignedData > [0] > encapContentInfo > [0] >
/// OCTET STRING`. Each level is checked rather than indexed blindly, because a
/// signature produced by a different tool may omit optional wrappers.
fn entitlements_from_blob(blob : Bytes) -> Bytes raise MachscopeError {
let top = read_element(blob, 0L, blob.length().to_int64())
let mut found : Bytes? = None
let stack : Array[DerElement] = [top]
for _ in 0..<24 {
if stack.is_empty() || found is Some(_) {
break
}
let el = stack.pop().unwrap()
if el.tag == TAG_OCTET_STRING {
found = Some(octet_string(blob, el))
} else {
let constructed = el.tag == TAG_SEQUENCE ||
el.tag == TAG_SET ||
el.tag == TAG_CTX_0 ||
el.tag == TAG_CTX_1
if constructed {
for child in children(blob, el) {
stack.push(child)
}
}
}
}
match found {
Some(bytes) => bytes
None =>
raise Inconsistent(
what="entitlements blob",
detail="no octet string holding a plist",
)
}
}