Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
a74924e
Branches
Tags
main
apple-crates
apple-codesign-quick
src
signature
entitlements.rs
Refactor and unify a lot of things for another project and implement airdrop support
Dadoum
commited
a74924e
at 2026-10-25 22:15:58
entitlements.rs
Blame
History
Raw
use super::{ CS_EXECSEG_ALLOW_UNSIGNED, CS_EXECSEG_CAN_EXEC_CDHASH, CS_EXECSEG_CAN_LOAD_CDHASH, CS_EXECSEG_DEBUGGER, CS_EXECSEG_JIT, CS_EXECSEG_MAIN_BINARY, CS_EXECSEG_SKIP_LV, }; use crate::error::{CodeSignError, Result}; use plist::{Dictionary, Value}; // Entitlements that codesign turns into executable segment flags. const EXEC_SEGMENT_ENTITLEMENTS: [(&str, u64); 7] = [ ("get-task-allow", CS_EXECSEG_ALLOW_UNSIGNED), ("run-unsigned-code", CS_EXECSEG_ALLOW_UNSIGNED), ("com.apple.private.cs.debugger", CS_EXECSEG_DEBUGGER), ("dynamic-codesigning", CS_EXECSEG_JIT), ( "com.apple.private.skip-library-validation", CS_EXECSEG_SKIP_LV, ), ( "com.apple.private.amfi.can-load-cdhash", CS_EXECSEG_CAN_LOAD_CDHASH, ), ( "com.apple.private.amfi.can-execute-cdhash", CS_EXECSEG_CAN_EXEC_CDHASH, ), ]; // Tags of Apple's DER entitlements, as CoreEntitlements writes them. const DER_BOOLEAN: u8 = 0x01; const DER_INTEGER: u8 = 0x02; const DER_UTF8_STRING: u8 = 0x0c; const DER_SEQUENCE: u8 = 0x30; const DER_SET: u8 = 0x31; const DER_ENTITLEMENTS_APPLICATION: u8 = 0x70; const DER_ENTITLEMENTS_DICTIONARY: u8 = 0xb0; pub fn entitlements_xml(entitlements: &Dictionary) -> Result<Vec<u8>> { let mut data = Vec::new(); plist::to_writer_xml(&mut data, entitlements)?; Ok(data) } pub fn entitlement_exec_flags(entitlements: &Dictionary, is_main_binary: bool) -> u64 { let main_binary = if is_main_binary { CS_EXECSEG_MAIN_BINARY } else { 0 }; EXEC_SEGMENT_ENTITLEMENTS .iter() .filter(|(key, _)| matches!(entitlements.get(key), Some(Value::Boolean(true)))) .fold(main_binary, |flags, (_, flag)| flags | flag) } /// Encode entitlements in Apple's DER form: a versioned application tag around the dictionary. pub fn der_entitlements(entitlements: &Dictionary) -> Result<Vec<u8>> { let dictionary = der_dictionary_body(entitlements, "$")?; let body = [ der_tlv(DER_INTEGER, &[1]), der_tlv(DER_ENTITLEMENTS_DICTIONARY, &dictionary), ] .concat(); Ok(der_tlv(DER_ENTITLEMENTS_APPLICATION, &body)) } fn der_value(value: &Value, path: &str) -> Result<Vec<u8>> { let unsupported = || CodeSignError::UnsupportedEntitlement(path.to_string()); match value { Value::Boolean(value) => Ok(der_tlv(DER_BOOLEAN, &[if *value { 0xff } else { 0x00 }])), Value::Integer(value) => value .as_signed() .map(i128::from) .or(value.as_unsigned().map(i128::from)) .map(der_integer) .ok_or_else(unsupported), Value::String(value) => Ok(der_tlv(DER_UTF8_STRING, value.as_bytes())), Value::Array(values) => { let body: Result<Vec<_>> = values .iter() .enumerate() .map(|(index, value)| der_value(value, &format!("{path}[{index}]"))) .collect(); Ok(der_tlv(DER_SEQUENCE, &body?.concat())) } Value::Dictionary(value) => Ok(der_tlv(DER_SET, &der_dictionary_body(value, path)?)), _ => Err(unsupported()), } } // Entries are sorted by key, which CoreEntitlements relies on for its lookups. fn der_dictionary_body(dictionary: &Dictionary, path: &str) -> Result<Vec<u8>> { let entries: Result<Vec<_>> = dictionary .iter() .map(|(key, value)| { let entry = [ der_tlv(DER_UTF8_STRING, key.as_bytes()), der_value(value, &format!("{path}.{key}"))?, ] .concat(); Ok((key.as_bytes(), der_tlv(DER_SEQUENCE, &entry))) }) .collect(); let mut entries = entries?; entries.sort_by_key(|(key, _)| *key); Ok(entries.into_iter().flat_map(|(_, entry)| entry).collect()) } // CoreEntitlements integers are int64s, as minimal two's complement. fn der_integer(value: i128) -> Vec<u8> { let bytes = value.to_be_bytes(); let redundant = bytes .windows(2) .take_while(|pair| { matches!(pair, [0x00, next] if next & 0x80 == 0) || matches!(pair, [0xff, next] if next & 0x80 != 0) }) .count(); der_tlv(DER_INTEGER, &bytes[redundant..]) } fn der_tlv(tag: u8, body: &[u8]) -> Vec<u8> { let mut out = Vec::with_capacity(1 + 9 + body.len()); out.push(tag); if body.len() < 0x80 { out.push(body.len() as u8); } else { let len = body.len().to_be_bytes(); let first_significant = len .iter() .position(|byte| *byte != 0) .unwrap_or(len.len() - 1); out.push(0x80 | (len.len() - first_significant) as u8); out.extend_from_slice(&len[first_significant..]); } out.extend_from_slice(body); out } #[cfg(test)] mod tests { use super::*; #[test] fn der_entitlements_use_apple_versioned_application_envelope() { assert_eq!( der_entitlements(&Dictionary::new()).unwrap(), [0x70, 0x05, 0x02, 0x01, 0x01, 0xb0, 0x00] ); } #[test] fn entitlement_dictionary_entries_are_sorted_by_key() { let entitlements = Dictionary::from_iter([ ("z", Value::Boolean(true)), ( "application-identifier", Value::String("TEAM.com.example.App".to_string()), ), ]); let body = der_dictionary_body(&entitlements, "$").unwrap(); let expected_first = der_tlv( 0x30, &[ der_tlv(0x0c, b"application-identifier"), der_tlv(0x0c, b"TEAM.com.example.App"), ] .concat(), ); assert!(body.starts_with(&expected_first)); } #[test] fn der_integers_and_long_lengths_are_minimal() { assert_eq!(der_integer(0), [0x02, 0x01, 0x00]); assert_eq!(der_integer(0x80), [0x02, 0x02, 0x00, 0x80]); assert_eq!(der_integer(0x1234), [0x02, 0x02, 0x12, 0x34]); assert_eq!(der_integer(-1), [0x02, 0x01, 0xff]); assert_eq!(der_integer(-129), [0x02, 0x02, 0xff, 0x7f]); assert_eq!(&der_tlv(0x0c, &[0; 0x100])[..4], [0x0c, 0x82, 0x01, 0x00]); assert_eq!(&der_tlv(0x0c, &[0; 0x80])[..3], [0x0c, 0x81, 0x80]); } #[test] fn exec_flags_follow_boolean_entitlements() { let entitlements = Dictionary::from_iter([ ("get-task-allow", Value::Boolean(true)), ("dynamic-codesigning", Value::Boolean(false)), ]); assert_eq!( entitlement_exec_flags(&entitlements, true), CS_EXECSEG_MAIN_BINARY | CS_EXECSEG_ALLOW_UNSIGNED ); assert_eq!( entitlement_exec_flags(&entitlements, false), CS_EXECSEG_ALLOW_UNSIGNED ); } }