// Apple's DER encoding of a property list.
//
// `CSMAGIC_EMBEDDED_DER_ENTITLEMENTS` (slot 7) does not hold an XML plist. It
// holds a property list encoded as ASN.1 DER with Apple's own tag assignments:
// the outer element is an application-class constructed tag 16 carrying a
// version integer and then the value, a dictionary is context-class constructed
// tag 16 whose children are SEQUENCE-wrapped key/value pairs, and an array is
// an ordinary SEQUENCE. The universal tags are the usual ones — BOOLEAN,
// INTEGER, UTF8String, OCTET STRING.
//
// Everything below is checked against the buffer. A DER plist that does not
// decode completely raises rather than returning a partial dictionary, because
// a caller reading entitlements is usually deciding whether to trust a build
// artifact and half a dictionary is the wrong answer to give it.
///|
/// The universal tag of a BOOLEAN.
pub const TAG_BOOLEAN : UInt32 = 0x01
///|
/// The universal tag of an INTEGER.
pub const TAG_INTEGER : UInt32 = 0x02
///|
/// Apple's tag for a whole DER-encoded plist: application class, constructed,
/// tag number 16.
pub const TAG_PLIST : UInt32 = 0x70
///|
/// Apple's tag for a dictionary: context class, constructed, tag number 16.
///
/// A dictionary's children are SEQUENCEs, each holding a key string and a
/// value. DER requires the members of a SET to be sorted, and Apple sorts these
/// by key, so the decoded order is stable across rebuilds.
pub const TAG_PLIST_DICT : UInt32 = 0xB0
///|
/// The deepest nesting this decoder accepts.
///
/// A plist is the one structure in a code signature whose shape is attacker
/// controlled and recursive, so the recursion needs a bound that does not
/// depend on the buffer being well formed.
const PLIST_MAX_DEPTH : Int32 = 64
///|
/// A decoded property list value.
///
/// The variants correspond one-to-one with the value types an entitlement file
/// may contain. `Date` is absent because Apple never encodes one here; a
/// plist that contains one is rejected rather than coerced into a string.
pub(all) enum Plist {
/// `` or ``.
Bool(Bool)
/// ``, sign-extended to 64 bits.
Int(Int64)
/// ``.
Str(String)
/// ``, as the raw bytes.
Data(Bytes)
/// ``, in encoded order.
Array(Array[Plist])
/// A ``; keys are in encoded order, which DER requires to be sorted.
Dict(Array[(String, Plist)])
}
///|
/// Looks a key up in a dictionary.
///
/// Returns `None` for a non-dictionary, so a caller can chain lookups without
/// matching on the variant at every level.
pub fn Plist::get(self : Plist, key : String) -> Plist? {
match self {
Dict(entries) => {
for entry in entries {
let (k, v) = entry
if k == key {
return Some(v)
}
}
None
}
_ => None
}
}
///|
/// Decodes a DER-encoded property list.
///
/// Accepts both the wrapped form Apple writes (a `TAG_PLIST` element holding a
/// version integer and the value) and a bare value, because the tag is not
/// always present in blobs produced by older toolchains.
pub fn decode_der_plist(bytes : Bytes) -> Plist raise MachscopeError {
let limit = bytes.length().to_int64()
let top = read_element(bytes, 0L, limit)
if top.tag == TAG_PLIST {
let kids = children(bytes, top)
if kids.length() != 2 {
raise Inconsistent(
what="DER plist",
detail="wrapper must hold a version and one value",
)
}
let version = plist_value(bytes, kids[0], 0)
if version is Int(v) && v != 1L {
raise BadValue(field="DER plist version", value=v)
}
return plist_value(bytes, kids[1], 0)
}
plist_value(bytes, top, 0)
}
///|
/// Decodes one value, dispatching on its tag.
///
/// `depth` counts nesting from the outermost element and is what keeps a plist
/// full of arrays from exhausting the call stack.
fn plist_value(
bytes : Bytes,
el : DerElement,
depth : Int32,
) -> Plist raise MachscopeError {
if depth > PLIST_MAX_DEPTH {
raise LimitExceeded(
what="DER plist nesting",
limit=PLIST_MAX_DEPTH.to_int64(),
)
}
match el.tag {
TAG_BOOLEAN => {
if el.content_end - el.content_start != 1L {
raise BadValue(
field="DER boolean length",
value=el.content_end - el.content_start,
)
}
Bool(bytes.at(el.content_start.to_int()).to_int() != 0)
}
TAG_INTEGER => Int(plist_integer(bytes, el))
TAG_UTF8 => Str(plist_string(bytes, el))
TAG_OCTET_STRING => Data(plist_bytes(bytes, el))
TAG_SEQUENCE => {
let out : Array[Plist] = []
for child in children(bytes, el) {
out.push(plist_value(bytes, child, depth + 1))
}
Array(out)
}
TAG_PLIST_DICT => {
let out : Array[(String, Plist)] = []
for pair in children(bytes, el) {
if pair.tag != TAG_SEQUENCE {
raise BadValue(field="DER dict entry tag", value=pair.tag.to_int64())
}
let members = children(bytes, pair)
if members.length() != 2 {
raise Inconsistent(
what="DER dict entry",
detail="expected exactly a key and a value",
)
}
if members[0].tag != TAG_UTF8 {
raise BadValue(
field="DER dict key tag",
value=members[0].tag.to_int64(),
)
}
out.push(
(
plist_string(bytes, members[0]),
plist_value(bytes, members[1], depth + 1),
),
)
}
Dict(out)
}
_ => raise Unsupported(what="DER plist value tag \{el.tag.to_int64()}")
}
}
///|
/// Reads an INTEGER's content as a big-endian two's-complement value.
///
/// DER does not pad to a fixed width, so an integer occupies as few bytes as it
/// can; a negative value is sign-extended from its own most significant bit.
fn plist_integer(bytes : Bytes, el : DerElement) -> Int64 raise MachscopeError {
let n = el.content_end - el.content_start
if n == 0L {
return 0L
}
if n > 8L {
raise LimitExceeded(what="DER integer width", limit=8L)
}
let mut v = 0L
for i in 0.. String raise MachscopeError {
let raw = plist_bytes(bytes, el)
@utf8.decode(raw) catch {
_ => raise BadName(offset=el.content_start)
}
}
///|
/// Copies an element's content range out of the buffer.
fn plist_bytes(bytes : Bytes, el : DerElement) -> Bytes raise MachscopeError {
let n = el.content_end - el.content_start
if n < 0L {
raise Inconsistent(
what="DER element",
detail="content range runs backwards",
)
}
let out = Buffer::Buffer(size_hint=n.to_int())
for i in 0.. Json {
match self {
Bool(b) => Json::boolean(b)
Int(v) => Json::number(v.to_double())
Str(s) => Json::string(s)
Data(d) => Json::string(hex_bytes(d))
Array(items) => {
let out : Array[Json] = []
for item in items {
out.push(item.to_json())
}
Json::array(out)
}
Dict(entries) => {
let fields : Map[String, Json] = Map([])
for entry in entries {
let (k, v) = entry
fields[k] = v.to_json()
}
Json::object(fields)
}
}
}
///|
/// Formats bytes as lowercase hex without separators.
fn hex_bytes(bytes : Bytes) -> String {
let digits = "0123456789abcdef"
let buf = StringBuilder()
for i in 0..> 4) & 0xF).unsafe_to_char())
buf.write_char(digits.code_unit_at(b & 0xF).unsafe_to_char())
}
buf.to_string()
}