1use crate::error::{Error, Result};
24
25const ENTRY_LEAD: u32 = 0x0000_0000;
27
28const SUBJECT_TAG: u32 = 0x0000_007b;
30
31const SUBJECT_LEAD: u32 = 0x0000_0001;
33
34const SUBJECT_TRAILER: [u32; 2] = [0x0101_0000, 0x0101_0000];
37
38const SUBJECT_ANY: u32 = 0x0000_0001;
40
41const SUBJECT_TRUSTED_APPLICATION: u32 = 0x0000_0074;
43
44const TRUSTED_APPLICATION_LEAD: u32 = 0x0000_0001;
46
47const LEGACY_HASH_LEN: usize = 20;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum EntryKind {
59 Owner,
61 Authorization,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum Authorization {
75 Tag35,
77 ItemAccess,
80 Tags(Vec<u32>),
82}
83
84impl Authorization {
85 const TAG_35: [u32; 1] = [35];
86 const ITEM_ACCESS: [u32; 6] = [24, 28, 37, 38, 59, 115];
87
88 pub fn from_tags(tags: Vec<u32>) -> Self {
89 if tags == Self::TAG_35 {
90 Self::Tag35
91 } else if tags == Self::ITEM_ACCESS {
92 Self::ItemAccess
93 } else {
94 Self::Tags(tags)
95 }
96 }
97
98 pub fn tags(&self) -> &[u32] {
99 match self {
100 Self::Tag35 => &Self::TAG_35,
101 Self::ItemAccess => &Self::ITEM_ACCESS,
102 Self::Tags(tags) => tags,
103 }
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct TrustedApplication {
115 pub path: String,
117 pub legacy_hash: [u8; LEGACY_HASH_LEN],
119 pub requirement: Vec<u8>,
122}
123
124impl TrustedApplication {
125 pub fn new(path: impl Into<String>, requirement: Vec<u8>) -> Self {
128 Self {
129 path: path.into(),
130 legacy_hash: [0u8; LEGACY_HASH_LEN],
131 requirement,
132 }
133 }
134
135 fn comment_len(&self) -> usize {
137 name_field_len(&self.path) + self.requirement.len()
138 }
139
140 fn encoded_len(&self) -> usize {
141 4 * 3 + LEGACY_HASH_LEN + 4 + self.comment_len()
144 }
145
146 fn write(&self, out: &mut Vec<u8>) {
147 push_words(
148 out,
149 &[SUBJECT_TRUSTED_APPLICATION, TRUSTED_APPLICATION_LEAD],
150 );
151 push_words(out, &[LEGACY_HASH_LEN as u32]);
152 out.extend_from_slice(&self.legacy_hash);
153 push_words(out, &[self.comment_len() as u32]);
154 out.extend_from_slice(&name_field(&self.path));
155 out.extend_from_slice(&self.requirement);
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum Subject {
162 Any,
165 TrustedApplications(Vec<TrustedApplication>),
167 Unknown(Vec<u32>),
169}
170
171impl Subject {
172 fn element_count(&self) -> usize {
174 match self {
175 Self::Any => 1,
176 Self::TrustedApplications(apps) => apps.len(),
177 Self::Unknown(_) => 1,
180 }
181 }
182
183 fn encoded_len(&self) -> usize {
185 match self {
186 Self::Any => 4,
187 Self::TrustedApplications(apps) => {
188 apps.iter().map(TrustedApplication::encoded_len).sum()
189 }
190 Self::Unknown(words) => 4 * words.len(),
191 }
192 }
193
194 fn write(&self, out: &mut Vec<u8>) {
195 match self {
196 Self::Any => push_words(out, &[SUBJECT_ANY]),
197 Self::TrustedApplications(apps) => {
198 for app in apps {
199 app.write(out);
200 }
201 }
202 Self::Unknown(words) => push_words(out, words),
203 }
204 }
205
206 pub fn trusted_paths(&self) -> Vec<&str> {
208 match self {
209 Self::TrustedApplications(apps) => apps.iter().map(|app| app.path.as_str()).collect(),
210 _ => Vec::new(),
211 }
212 }
213}
214
215#[derive(Debug, Clone, PartialEq, Eq)]
217pub struct AclEntry {
218 pub kind: EntryKind,
219 pub subject: Option<Subject>,
222 pub name: String,
224 pub authorization: Option<Authorization>,
226 pub authorization_prefix: [u32; 2],
228}
229
230impl AclEntry {
231 fn element_word(&self) -> u32 {
233 1 + self.subject.as_ref().map_or(0, Subject::element_count) as u32
234 }
235
236 fn encoded_len(&self) -> usize {
237 let mut len = 4 * 4 + 4 * SUBJECT_TRAILER.len() + name_field_len(&self.name);
239 if let Some(subject) = &self.subject {
240 len += subject.encoded_len();
241 }
242 if let Some(authorization) = &self.authorization {
243 len += 4 * (2 + 1 + authorization.tags().len());
244 }
245 len
246 }
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
251pub struct AclBlob {
252 pub owner: AclEntry,
254 pub entries: Vec<AclEntry>,
255}
256
257impl AclBlob {
258 pub fn for_item(name: &str) -> Self {
261 Self::for_item_with_subject(name, Subject::Any)
262 }
263
264 pub fn for_item_trusting(name: &str, applications: Vec<TrustedApplication>) -> Self {
271 Self::for_item_with_subject(name, Subject::TrustedApplications(applications))
272 }
273
274 pub fn for_item_with_subject(name: &str, subject: Subject) -> Self {
277 Self {
278 owner: AclEntry {
279 kind: EntryKind::Owner,
280 subject: None,
281 name: name.to_string(),
282 authorization: None,
283 authorization_prefix: [0, 0],
284 },
285 entries: vec![
286 AclEntry {
287 kind: EntryKind::Authorization,
288 subject: Some(Subject::Any),
289 name: name.to_string(),
290 authorization: Some(Authorization::Tag35),
291 authorization_prefix: [0, 0],
292 },
293 AclEntry {
294 kind: EntryKind::Authorization,
295 subject: Some(subject),
296 name: name.to_string(),
297 authorization: Some(Authorization::ItemAccess),
298 authorization_prefix: [0, 0],
299 },
300 ],
301 }
302 }
303
304 pub fn trusted_paths(&self) -> Vec<&str> {
306 self.entries
307 .iter()
308 .flat_map(|entry| entry.subject.iter().flat_map(Subject::trusted_paths))
309 .collect()
310 }
311
312 pub fn parse(data: &[u8]) -> Result<Self> {
313 let mut reader = WordReader { data, at: 0 };
314 let owner = reader.entry(EntryKind::Owner)?;
315 let count = reader.u32()? as usize;
316 if count > 64 {
317 return Err(Error::format(format!("ACL claims {count} entries")));
318 }
319 let mut entries = Vec::with_capacity(count);
320 for _ in 0..count {
321 entries.push(reader.entry(EntryKind::Authorization)?);
322 }
323 if reader.at != data.len() {
324 return Err(Error::format(format!(
325 "ACL has {} trailing bytes",
326 data.len() - reader.at
327 )));
328 }
329 Ok(Self { owner, entries })
330 }
331
332 pub fn to_bytes(&self) -> Vec<u8> {
333 let mut out = Vec::with_capacity(self.encoded_len());
334 write_entry(&mut out, &self.owner);
335 out.extend_from_slice(&(self.entries.len() as u32).to_be_bytes());
336 for entry in &self.entries {
337 write_entry(&mut out, entry);
338 }
339 out
340 }
341
342 pub fn encoded_len(&self) -> usize {
343 self.owner.encoded_len()
344 + 4
345 + self
346 .entries
347 .iter()
348 .map(AclEntry::encoded_len)
349 .sum::<usize>()
350 }
351
352 pub fn item_name(&self) -> Option<&str> {
354 let name = self.owner.name.as_str();
355 self.entries
356 .iter()
357 .all(|entry| entry.name == name)
358 .then_some(name)
359 }
360}
361
362fn write_entry(out: &mut Vec<u8>, entry: &AclEntry) {
363 push_words(
364 out,
365 &[ENTRY_LEAD, SUBJECT_TAG, entry.element_word(), SUBJECT_LEAD],
366 );
367 if let Some(subject) = &entry.subject {
368 subject.write(out);
369 }
370 push_words(out, &SUBJECT_TRAILER);
371 out.extend_from_slice(&name_field(&entry.name));
372 if let Some(authorization) = &entry.authorization {
373 for word in entry.authorization_prefix {
374 out.extend_from_slice(&word.to_be_bytes());
375 }
376 out.extend_from_slice(&(authorization.tags().len() as u32).to_be_bytes());
377 for tag in authorization.tags() {
378 out.extend_from_slice(&tag.to_be_bytes());
379 }
380 }
381}
382
383struct WordReader<'a> {
384 data: &'a [u8],
385 at: usize,
386}
387
388impl WordReader<'_> {
389 fn u32(&mut self) -> Result<u32> {
390 let bytes = self
391 .data
392 .get(self.at..self.at + 4)
393 .ok_or_else(|| Error::format("ACL ends mid-word"))?;
394 self.at += 4;
395 Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
396 }
397
398 fn entry(&mut self, kind: EntryKind) -> Result<AclEntry> {
399 let lead = self.u32()?;
400 if lead != ENTRY_LEAD {
401 return Err(Error::format(format!("ACL entry starts with 0x{lead:08x}")));
402 }
403 let tag = self.u32()?;
404 if tag != SUBJECT_TAG {
405 return Err(Error::format(format!("ACL subject tag is 0x{tag:08x}")));
406 }
407
408 let element_word = self.u32()? as usize;
410 let elements = element_word
411 .checked_sub(1)
412 .ok_or_else(|| Error::format("ACL entry claims zero elements"))?;
413 if elements > 64 {
414 return Err(Error::format(format!(
415 "ACL entry claims {elements} subject elements"
416 )));
417 }
418 match kind {
419 EntryKind::Owner if elements != 0 => {
420 return Err(Error::format(format!(
421 "the owner entry carries {elements} subject elements"
422 )));
423 }
424 EntryKind::Authorization if elements == 0 => {
425 return Err(Error::format("an authorization entry carries no subject"));
426 }
427 _ => {}
428 }
429
430 let subject_lead = self.u32()?;
431 if subject_lead != SUBJECT_LEAD {
432 return Err(Error::format(format!(
433 "ACL subject lead is 0x{subject_lead:08x}"
434 )));
435 }
436
437 let subject = match kind {
440 EntryKind::Owner => None,
441 EntryKind::Authorization => Some(self.subject(elements)?),
442 };
443
444 for word in SUBJECT_TRAILER {
445 let found = self.u32()?;
446 if found != word {
447 return Err(Error::format(format!(
448 "ACL subject trailer is 0x{found:08x}, expected 0x{word:08x}"
449 )));
450 }
451 }
452
453 let name = self.name()?;
454 let (authorization, authorization_prefix) = match kind {
455 EntryKind::Owner => (None, [0, 0]),
456 EntryKind::Authorization => {
457 let prefix = [self.u32()?, self.u32()?];
458 let count = self.u32()? as usize;
459 if count > 64 {
460 return Err(Error::format(format!("ACL entry claims {count} tags")));
461 }
462 let mut tags = Vec::with_capacity(count);
463 for _ in 0..count {
464 tags.push(self.u32()?);
465 }
466 (Some(Authorization::from_tags(tags)), prefix)
467 }
468 };
469
470 Ok(AclEntry {
471 kind,
472 subject,
473 name,
474 authorization,
475 authorization_prefix,
476 })
477 }
478
479 fn subject(&mut self, elements: usize) -> Result<Subject> {
482 match self.peek()? {
483 SUBJECT_ANY if elements == 1 => {
484 self.u32()?;
485 Ok(Subject::Any)
486 }
487 SUBJECT_TRUSTED_APPLICATION => {
488 let mut applications = Vec::with_capacity(elements);
489 for _ in 0..elements {
490 applications.push(self.trusted_application()?);
491 }
492 Ok(Subject::TrustedApplications(applications))
493 }
494 other => Err(Error::format(format!(
495 "unknown ACL subject type 0x{other:08x} with {elements} elements"
496 ))),
497 }
498 }
499
500 fn trusted_application(&mut self) -> Result<TrustedApplication> {
501 self.u32()?; let lead = self.u32()?;
503 if lead != TRUSTED_APPLICATION_LEAD {
504 return Err(Error::format(format!(
505 "trusted-application lead is 0x{lead:08x}"
506 )));
507 }
508
509 let hash_len = self.u32()? as usize;
510 if hash_len != LEGACY_HASH_LEN {
511 return Err(Error::format(format!(
512 "trusted-application hash is {hash_len} bytes, expected {LEGACY_HASH_LEN}"
513 )));
514 }
515 let hash = self.bytes(LEGACY_HASH_LEN)?;
516
517 let comment_len = self.u32()? as usize;
519 let comment = self.bytes(comment_len)?;
520 let path_end = comment
521 .iter()
522 .position(|byte| *byte == 0)
523 .ok_or_else(|| Error::format("trusted-application path is not terminated"))?;
524 let path = String::from_utf8_lossy(&comment[..path_end]).into_owned();
525 let requirement = comment[name_field_len(&path)..].to_vec();
526
527 Ok(TrustedApplication {
528 path,
529 legacy_hash: hash.try_into().expect("checked length"),
530 requirement,
531 })
532 }
533
534 fn peek(&self) -> Result<u32> {
535 let bytes = self
536 .data
537 .get(self.at..self.at + 4)
538 .ok_or_else(|| Error::format("ACL ends mid-word"))?;
539 Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
540 }
541
542 fn bytes(&mut self, len: usize) -> Result<Vec<u8>> {
543 let bytes = self
544 .data
545 .get(self.at..self.at + len)
546 .ok_or_else(|| Error::format("ACL field runs past the blob"))?
547 .to_vec();
548 self.at += len;
549 Ok(bytes)
550 }
551
552 fn name(&mut self) -> Result<String> {
554 let rest = self
555 .data
556 .get(self.at..)
557 .ok_or_else(|| Error::format("ACL ends at a name"))?;
558 let end = rest
559 .iter()
560 .position(|byte| *byte == 0)
561 .ok_or_else(|| Error::format("ACL name is not terminated"))?;
562 let name = String::from_utf8_lossy(&rest[..end]).into_owned();
563 self.at += name_field_len(&name);
564 Ok(name)
565 }
566}
567
568fn name_field_len(name: &str) -> usize {
571 (name.len() + 1 + 3) & !3
572}
573
574fn push_words(out: &mut Vec<u8>, words: &[u32]) {
575 for word in words {
576 out.extend_from_slice(&word.to_be_bytes());
577 }
578}
579
580fn name_field(name: &str) -> Vec<u8> {
581 let mut field = vec![0u8; name_field_len(name)];
582 field[..name.len()].copy_from_slice(name.as_bytes());
583 field
584}
585
586pub fn item_public_acl(item_name: &str) -> Vec<u8> {
588 AclBlob::for_item(item_name).to_bytes()
589}
590
591pub fn database_public_acl() -> Vec<u8> {
594 DATABASE_PUBLIC_ACL.to_vec()
595}
596
597const DATABASE_PUBLIC_ACL: [u8; 28] = [
598 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
599 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
600];
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[test]
607 fn name_field_is_nul_terminated_and_padded() {
608 assert_eq!(name_field("a"), b"a\0\0\0");
609 assert_eq!(name_field("abcd"), b"abcd\0\0\0\0");
610 assert_eq!(name_field("abcdefgh"), b"abcdefgh\0\0\0\0");
611 assert_eq!(name_field("abcdefghi"), b"abcdefghi\0\0\0");
612 assert_eq!(name_field("example.com"), b"example.com\0");
613 assert_eq!(name_field(""), b"\0\0\0\0");
614 }
615
616 #[test]
617 fn built_acl_round_trips_through_its_own_parser() {
618 for name in ["a", "abcd", "myservice", "example.com", "abcdefghijkl", ""] {
619 let blob = AclBlob::for_item(name);
620 let bytes = blob.to_bytes();
621 assert_eq!(
622 bytes.len(),
623 blob.encoded_len(),
624 "length agrees for {name:?}"
625 );
626
627 let parsed = AclBlob::parse(&bytes).unwrap();
628 assert_eq!(parsed, blob, "structure survives a round trip for {name:?}");
629 assert_eq!(parsed.to_bytes(), bytes);
630 assert_eq!(parsed.item_name(), Some(name));
631 }
632 }
633
634 #[test]
635 fn built_acl_has_the_expected_structure() {
636 let blob = AclBlob::for_item("myservice");
637 assert_eq!(blob.owner.kind, EntryKind::Owner);
638 assert!(blob.owner.authorization.is_none());
639 assert_eq!(blob.entries.len(), 2);
640 assert_eq!(blob.entries[0].authorization, Some(Authorization::Tag35));
641 assert_eq!(
642 blob.entries[1].authorization,
643 Some(Authorization::ItemAccess)
644 );
645 assert!(blob.owner.subject.is_none());
647 assert_eq!(blob.entries[0].subject, Some(Subject::Any));
648 }
649
650 #[test]
652 fn acl_lengths_match_the_observed_samples() {
653 for (name, expected) in [
654 ("a", 148),
655 ("abcd", 160),
656 ("abcdefgh", 172),
657 ("abcdefghijkl", 184),
658 ("abcdefghi", 172),
659 ("myservice", 172),
660 ("other", 160),
661 ("example.com", 172),
662 ] {
663 assert_eq!(item_public_acl(name).len(), expected, "name {name:?}");
664 }
665 }
666
667 #[test]
670 fn acl_bytes_match_the_observed_sample() {
671 let expected = concat!(
672 "00000000", "0000007b", "00000001", "00000001", "01010000", "01010000", "61000000",
673 "00000002", "00000000", "0000007b", "00000002", "00000001", "00000001", "01010000", "01010000",
675 "61000000", "00000000", "00000000", "00000001", "00000023", "00000000", "0000007b", "00000002", "00000001", "00000001", "01010000", "01010000",
677 "61000000", "00000000", "00000000", "00000006", "00000018", "0000001c", "00000025",
678 "00000026", "0000003b", "00000073",
679 );
680 let parsed = AclBlob::parse(&hex::decode(expected).unwrap()).unwrap();
683 let generated = AclBlob::for_item("a");
684 assert_eq!(parsed.owner, generated.owner);
685 assert_eq!(sorted_entries(&parsed), sorted_entries(&generated));
686 assert_eq!(parsed.encoded_len(), generated.encoded_len());
687 }
688
689 fn sorted_entries(blob: &AclBlob) -> Vec<AclEntry> {
692 let mut entries = blob.entries.clone();
693 entries.sort_by_key(|entry| {
694 entry
695 .authorization
696 .as_ref()
697 .map(|authorization| authorization.tags().to_vec())
698 });
699 entries
700 }
701
702 #[test]
703 fn parser_rejects_malformed_blobs() {
704 assert!(AclBlob::parse(&[]).is_err());
705 assert!(AclBlob::parse(&[0u8; 8]).is_err(), "no subject tag");
706
707 let mut bytes = item_public_acl("a");
710 bytes.push(0);
711 assert!(AclBlob::parse(&bytes).is_err());
712
713 let mut bytes = item_public_acl("a");
715 let len = bytes.len();
716 bytes[24..len.min(28)].fill(0x41);
717 assert!(AclBlob::parse(&bytes).is_err());
718 }
719
720 #[test]
721 fn authorization_tag_sets_are_recognized_and_preserved() {
722 assert_eq!(Authorization::from_tags(vec![35]), Authorization::Tag35);
723 assert_eq!(
724 Authorization::from_tags(vec![24, 28, 37, 38, 59, 115]),
725 Authorization::ItemAccess
726 );
727 let other = Authorization::from_tags(vec![1, 2]);
728 assert_eq!(other, Authorization::Tags(vec![1, 2]));
729 assert_eq!(other.tags(), &[1, 2]);
730 }
731
732 const TRUSTED_SUBJECT_SAMPLE: &str = concat!(
736 "00000074", "00000001", "00000014", "014b034370a7a0b4b319a58e182cc37a320784e2",
740 "00000044", "2f7573722f62696e2f7365637572697479000000", "fade0c000000003000000001000000060000000200000012",
745 "636f6d2e6170706c652e7365637572697479000000000003",
746 );
747
748 fn sample_application() -> TrustedApplication {
749 TrustedApplication {
750 path: "/usr/bin/security".to_string(),
751 legacy_hash: hex::decode("014b034370a7a0b4b319a58e182cc37a320784e2")
752 .unwrap()
753 .try_into()
754 .unwrap(),
755 requirement: hex::decode(concat!(
756 "fade0c000000003000000001000000060000000200000012",
757 "636f6d2e6170706c652e7365637572697479000000000003",
758 ))
759 .unwrap(),
760 }
761 }
762
763 #[test]
764 fn a_trusted_application_block_matches_the_bytes_macos_wrote() {
765 let mut out = Vec::new();
766 sample_application().write(&mut out);
767 assert_eq!(hex::encode(&out), TRUSTED_SUBJECT_SAMPLE.replace(' ', ""));
768 }
769
770 #[test]
771 fn trusted_application_acls_round_trip() {
772 for applications in [
773 vec![sample_application()],
774 vec![
775 sample_application(),
776 TrustedApplication::new("/bin/ls", vec![0xfa, 0xde, 0x0c, 0x00, 0, 0, 0, 8]),
777 ],
778 ] {
779 let blob = AclBlob::for_item_trusting("item", applications.clone());
780 let bytes = blob.to_bytes();
781 assert_eq!(bytes.len(), blob.encoded_len());
782
783 let parsed = AclBlob::parse(&bytes).unwrap();
784 assert_eq!(parsed, blob);
785 assert_eq!(
786 parsed.trusted_paths(),
787 applications
788 .iter()
789 .map(|app| app.path.as_str())
790 .collect::<Vec<_>>()
791 );
792 assert_eq!(parsed.entries[0].subject, Some(Subject::Any));
794 assert_eq!(
795 parsed.entries[1].authorization,
796 Some(Authorization::ItemAccess),
797 "the restricted entry must be the one macOS restricts"
798 );
799 assert!(parsed.owner.subject.is_none());
800 }
801 }
802
803 #[test]
804 fn a_new_trusted_application_leaves_the_legacy_hash_zeroed() {
805 let app = TrustedApplication::new("/bin/ls", vec![0xfa, 0xde, 0x0c, 0x00, 0, 0, 0, 8]);
806 assert_eq!(app.legacy_hash, [0u8; LEGACY_HASH_LEN]);
807 let blob = AclBlob::for_item_trusting("x", vec![app]);
809 assert_eq!(AclBlob::parse(&blob.to_bytes()).unwrap(), blob);
810 }
811
812 #[test]
813 fn an_allow_any_acl_reports_no_trusted_paths() {
814 assert!(AclBlob::for_item("x").trusted_paths().is_empty());
815 }
816
817 #[test]
818 fn database_acl_is_the_observed_length() {
819 assert_eq!(database_public_acl().len(), 28);
820 }
821}