// A minimal DER/BER reader, used to reach the entitlements inside a code
// signature.
//
// Apple wraps entitlements in a PKCS#7 `SignedData` structure: the entitlement
// plist is not stored as a separate blob but as an octet string inside an
// attribute. Reaching it means walking ASN.1, so this file implements exactly
// the subset that walk needs — definite and indefinite lengths, the universal
// tags for sequences, octet strings and OIDs — and refuses everything else
// rather than guessing.
//
// Not a general ASN.1 library. It returns the payload or an error, never
// partial DER.
///|
/// One DER element: its tag, its content range and the offset just past it.
pub(all) struct DerElement {
/// The identifier octet. Only single-byte tags are decoded, which covers
/// every tag that appears in a code signature.
tag : UInt32
/// First byte of the content.
content_start : Int64
/// One past the last content byte.
content_end : Int64
/// One past the last byte of the whole element, header included.
next : Int64
}
///|
/// The universal tag number for a SEQUENCE or SEQUENCE OF.
pub const TAG_SEQUENCE : UInt32 = 0x30
///|
/// The universal tag number for a SET or SET OF.
pub const TAG_SET : UInt32 = 0x31
///|
/// The universal tag number for an OCTET STRING.
pub const TAG_OCTET_STRING : UInt32 = 0x04
///|
/// The universal tag number for an OBJECT IDENTIFIER.
pub const TAG_OID : UInt32 = 0x06
///|
/// The universal tag number for a UTF8String.
pub const TAG_UTF8 : UInt32 = 0x0C
///|
/// The tag Apple uses for a context-specific constructed element (0xA0).
pub const TAG_CTX_0 : UInt32 = 0xA0
///|
/// The second context-specific constructed tag in the CMS layout (0xA1).
pub const TAG_CTX_1 : UInt32 = 0xA1
///|
/// Encodes a string as UTF-8 bytes.
///
/// The DER payloads this module handles are ASCII identifiers and plist text,
/// so a byte-for-byte widening is exact; `@encoding/utf8` would be the general
/// answer but pulling in the encoder for this would be heavier than the need.
fn utf8_of(s : String) -> Bytes {
let buf = Buffer()
for i in 0.. DerElement raise MachscopeError {
if offset + 2L > limit {
raise UnexpectedEnd(offset~, needed=2L, available=limit - offset)
}
let tag = bytes.at(offset.to_int()).to_int().reinterpret_as_uint()
let first_len = bytes.at(offset.to_int() + 1).to_int()
let mut cursor = offset + 2L
let content_len : Int64? = if first_len < 0x80 {
Some(first_len.to_int64())
} else if first_len == 0x80 {
None // indefinite: caller walks children until end-of-contents
} else {
let n = first_len & 0x7F
if n > 4 {
raise Unsupported(what="DER length longer than 32 bits")
}
let mut len = 0L
for _ in 0.. limit {
raise UnexpectedEnd(offset=cursor, needed=1L, available=limit - cursor)
}
len = (len << 8) | bytes.at(cursor.to_int()).to_int().to_int64()
cursor = cursor + 1L
}
Some(len)
}
match content_len {
Some(len) => {
if cursor + len > limit {
raise OutOfBounds(what="DER content", offset=cursor, size=len, limit~)
}
DerElement::{
tag,
content_start: cursor,
content_end: cursor + len,
next: cursor + len,
}
}
None => {
// Walk children until the two zero bytes that close the element.
let mut at = cursor
let mut end = -1L
while at + 2L <= limit {
let t = bytes.at(at.to_int()).to_int()
let l = bytes.at(at.to_int() + 1).to_int()
if t == 0 && l == 0 {
end = at + 2L
break
}
let child = read_element(bytes, at, limit)
at = child.next
}
if end < 0L {
raise UnexpectedEnd(offset~, needed=2L, available=limit - at)
}
DerElement::{ tag, content_start: cursor, content_end: at, next: end, }
}
}
}
///|
/// Collects the immediate children of a constructed element.
///
/// Indefinite-length parents have their children walked until the
/// end-of-contents marker; definite-length parents stop at their declared end.
pub fn children(
bytes : Bytes,
parent : DerElement,
) -> Array[DerElement] raise MachscopeError {
let out : Array[DerElement] = []
let mut at = parent.content_start
while at < parent.content_end {
// Skip the end-of-contents marker of an indefinite element.
if parent.content_end - at >= 2L {
let t = bytes.at(at.to_int()).to_int()
let l = bytes.at(at.to_int() + 1).to_int()
if t == 0 && l == 0 {
break
}
}
let child = read_element(bytes, at, parent.content_end)
out.push(child)
at = child.next
}
out
}
///|
/// Reads an OCTET STRING's payload.
///
/// Concatenates the fragments of a constructed octet string, which is the form
/// a large entitlement plist takes.
pub fn octet_string(
bytes : Bytes,
el : DerElement,
) -> Bytes raise MachscopeError {
let out = StringBuilder()
if el.tag == TAG_OCTET_STRING && el.content_start + 2L <= el.content_end {
// Constructed form: children are octet strings.
let first = bytes.at(el.content_start.to_int()).to_int()
if first == 0x04 {
// Ambiguous: a primitive octet string whose first byte happens to be
// 0x04 starts with a tag byte too. Distinguish by re-reading the
// element and checking whether its declared content length matches.
let as_primitive_len = el.content_end - el.content_start
let inner = read_element(bytes, el.content_start, el.content_end)
if inner.content_end != el.content_start + as_primitive_len {
for part in children(bytes, el) {
for i in 0..<(part.content_end - part.content_start).to_int() {
out.write_char(
bytes.at((part.content_start + i.to_int64()).to_int()).to_char(),
)
}
}
return utf8_of(out.to_string())
}
}
}
for i in 0..<(el.content_end - el.content_start).to_int() {
out.write_char(
bytes.at((el.content_start + i.to_int64()).to_int()).to_char(),
)
}
utf8_of(out.to_string())
}
///|
/// Finds a `SignedData` element inside a code-signature blob.
///
/// The blob is a DER `ContentInfo`: a SEQUENCE holding an OID and a
/// context-specific wrapper around the `SignedData`. The walk returns the
/// SignedData element so its nested attributes can be searched.
pub fn find_signed_data(bytes : Bytes) -> DerElement raise MachscopeError {
let top = read_element(bytes, 0L, bytes.length().to_int64())
if top.tag != TAG_SEQUENCE {
raise BadValue(field="ContentInfo tag", value=top.tag.to_int64())
}
for child in children(bytes, top) {
if child.tag == TAG_SEQUENCE {
// Some blobs wrap SignedData directly in the second child.
return child
}
if child.tag == TAG_CTX_0 || child.tag == TAG_CTX_1 {
for inner in children(bytes, child) {
if inner.tag == TAG_SEQUENCE {
return inner
}
}
}
}
raise Inconsistent(what="PKCS#7 blob", detail="no SignedData sequence found")
}