1use crate::acl::TrustedApplication;
13use crate::error::{Error, Result};
14
15pub fn from_blob(path: impl Into<String>, requirement: Vec<u8>) -> Result<TrustedApplication> {
20 if requirement.len() < 8 || requirement[..4] != [0xfa, 0xde, 0x0c, 0x00] {
21 return Err(Error::other(
22 "a designated requirement must begin with the 0xfade0c00 magic",
23 ));
24 }
25 let declared = u32::from_be_bytes([
26 requirement[4],
27 requirement[5],
28 requirement[6],
29 requirement[7],
30 ]) as usize;
31 if declared != requirement.len() {
32 return Err(Error::other(format!(
33 "requirement declares {declared} bytes but is {}",
34 requirement.len()
35 )));
36 }
37 Ok(TrustedApplication::new(path, requirement))
38}
39
40#[cfg(feature = "trust-apps")]
43pub fn for_application(path: &std::path::Path) -> Result<TrustedApplication> {
44 let bytes = std::fs::read(path)
45 .map_err(|source| Error::io(format!("could not read {}", path.display()), source))?;
46 let container = macho_core::parse(&bytes).map_err(|error| {
47 Error::other(format!("{} is not a Mach-O image: {error}", path.display()))
48 })?;
49
50 for macho in container.macho_files() {
53 let signature = macho_codesign::parse_code_signature(macho).map_err(|error| {
54 Error::other(format!(
55 "{} has no readable code signature: {error}",
56 path.display()
57 ))
58 })?;
59 if let Some(requirement) = signature.designated_requirement() {
60 return from_blob(path.to_string_lossy().into_owned(), requirement.to_vec());
61 }
62 }
63 Err(Error::other(format!(
64 "{} has no designated requirement; it may be unsigned",
65 path.display()
66 )))
67}
68
69#[cfg(not(feature = "trust-apps"))]
71pub fn for_application(path: &std::path::Path) -> Result<TrustedApplication> {
72 Err(Error::other(format!(
73 "cannot read the designated requirement of {} — rebuild with `--features trust-apps`, \
74 or pass a requirement blob from `csreq -b`",
75 path.display()
76 )))
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 fn sample() -> Vec<u8> {
86 hex::decode(concat!(
87 "fade0c000000003000000001000000060000000200000012",
88 "636f6d2e6170706c652e7365637572697479000000000003",
89 ))
90 .unwrap()
91 }
92
93 #[test]
94 fn accepts_a_well_formed_requirement_blob() {
95 let app = from_blob("/usr/bin/security", sample()).unwrap();
96 assert_eq!(app.path, "/usr/bin/security");
97 assert_eq!(app.requirement, sample());
98 assert_eq!(
99 app.legacy_hash, [0u8; 20],
100 "left zeroed: it is not evaluated"
101 );
102 }
103
104 #[test]
105 fn rejects_anything_that_is_not_a_requirement() {
106 assert!(from_blob("/bin/ls", Vec::new()).is_err());
107 assert!(from_blob("/bin/ls", b"designated => anchor apple".to_vec()).is_err());
108
109 let mut truncated = sample();
111 truncated.truncate(16);
112 assert!(from_blob("/bin/ls", truncated).is_err());
113 }
114
115 #[cfg(feature = "trust-apps")]
116 #[test]
117 fn reads_the_requirement_out_of_a_signed_binary() {
118 let app = for_application(std::path::Path::new("/usr/bin/security")).unwrap();
119 assert_eq!(app.requirement, sample(), "must match what the ACL embeds");
120 }
121
122 #[cfg(not(feature = "trust-apps"))]
123 #[test]
124 fn without_the_feature_the_error_says_what_to_do() {
125 let error = for_application(std::path::Path::new("/usr/bin/security")).unwrap_err();
126 assert!(error.to_string().contains("trust-apps"));
127 assert!(error.to_string().contains("csreq"));
128 }
129}