// 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
///|
/// `CSSLOT_ENTITLEMENTS`: the entitlements payload as plist XML, uncompressed
/// and not wrapped in CMS.
///
/// The value is 5. This slot carries `CSMAGIC_EMBEDDED_ENTITLEMENTS`.
pub const CSSLOT_ENTITLEMENTS : UInt32 = 5
///|
/// `CSSLOT_DER_ENTITLEMENTS`: the same entitlements, DER-encoded.
///
/// The value is 7. Apple writes this slot alongside slot 5 so that a reader
/// that understands DER does not have to parse XML; the two describe the same
/// dictionary. A signature may carry either, or both.
pub const CSSLOT_DER_ENTITLEMENTS : UInt32 = 7
///|
/// `CSSLOT_SIGNATURESLOT`: the CMS/PKCS#7 signature blob.
///
/// The value is 0x10000. This is a signature *over* the code directory. It
/// holds no entitlements despite the `PKCS#7` in its name, which is what made
/// an earlier version of this file look in the wrong place.
pub const CSSLOT_SIGNATURESLOT : UInt32 = 0x0001_0000
///|
/// `CSMAGIC_EMBEDDED_ENTITLEMENTS`: an XML property list, uncompressed.
pub const CSMAGIC_EMBEDDED_ENTITLEMENTS : UInt32 = 0xFADE_7171
///|
/// `CSMAGIC_EMBEDDED_DER_ENTITLEMENTS`: the same plist, DER-encoded.
pub const CSMAGIC_EMBEDDED_DER_ENTITLEMENTS : UInt32 = 0xFADE_7172
///|
/// 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,
}
}
///|
/// The entitlements payload of a signature, as Apple stored it.
///
/// A signature may carry both forms; the DER one is returned preferentially
/// because it is the form the platform reads first and the XML one may be a
/// leftover from an earlier signing.
pub(all) struct Entitlements {
/// The blob magic that was found: `CSMAGIC_EMBEDDED_ENTITLEMENTS` for XML,
/// `CSMAGIC_EMBEDDED_DER_ENTITLEMENTS` for DER.
magic : UInt32
/// The decoded property list.
value : Plist
}
///|
/// Reads the entitlements of a signature, if it carries any.
///
/// Entitlements appear in two slots: `CSSLOT_DER_ENTITLEMENTS` (7) holds the
/// property list in Apple's DER encoding, and `CSSLOT_ENTITLEMENTS` (5) holds
/// the same dictionary as plist XML. The DER form is preferred when both are
/// present because it is what the platform reads, and because XML text is the
/// form that survives a re-sign but no longer describes the running binary.
///
/// A blob in either slot is an eight-byte header — magic and length — followed
/// by the payload. The `PKCS#7` blob at `CSSLOT_SIGNATURESLOT` is a signature
/// over the code directory and is deliberately not consulted.
///
/// Returns `None` when the signature carries neither slot, which is the normal
/// case for a binary signed without an entitlements file.
pub fn entitlements(
slice : MachSlice,
region : SignatureRegion,
) -> Entitlements? raise MachscopeError {
let refs = signature_blobs(slice, region)
let mut der : SignatureBlobRef? = None
let mut xml : SignatureBlobRef? = None
for r in refs {
let is_der = r.slot == CSSLOT_DER_ENTITLEMENTS &&
r.magic == CSMAGIC_EMBEDDED_DER_ENTITLEMENTS
let is_xml = r.slot == CSSLOT_ENTITLEMENTS &&
r.magic == CSMAGIC_EMBEDDED_ENTITLEMENTS
if is_der {
der = Some(r)
} else if is_xml {
xml = Some(r)
}
}
match (der, xml) {
(Some(r), _) => {
let blob = blob_payload(slice, region, r, "DER entitlements")
Some(Entitlements::{ magic: r.magic, value: decode_der_plist(blob), })
}
(None, Some(r)) => {
let blob = blob_payload(slice, region, r, "entitlements")
// The XML form is returned as a string: decoding it into `Plist` here
// would mean shipping an XML parser, and a caller that wants the
// dictionary should prefer the DER slot, which this tree does.
let text = @utf8.decode(blob) catch {
_ =>
raise Inconsistent(
what="entitlements blob",
detail="XML payload is not valid UTF-8",
)
}
Some(Entitlements::{ magic: r.magic, value: Str(text), })
}
(None, None) => None
}
}
///|
/// Copies a blob's payload, excluding its eight-byte magic and length header.
///
/// The header's own length is checked against the superblob index so that a
/// blob claiming more bytes than the index recorded is rejected instead of
/// silently truncated.
fn blob_payload(
slice : MachSlice,
region : SignatureRegion,
entry : SignatureBlobRef,
what : String,
) -> Bytes raise MachscopeError {
if entry.length < 8L {
raise Inconsistent(what~, detail="blob is shorter than its own header")
}
if entry.length > region.size {
raise Inconsistent(what~, detail="blob extends past the signature region")
}
let base = region.offset + entry.offset
slice.bounds.slice(base + 8L, entry.length - 8L, what)
}