Skip to main content

kc/
requirement.rs

1//! Designated requirements for trusted-application ACLs.
2//!
3//! An ACL entry that names a trusted application embeds that application's
4//! designated requirement — byte for byte the blob in its own code signature,
5//! magic and length header included. There is nothing to compute: the blob is
6//! copied out of the binary.
7//!
8//! With the `trust-apps` feature, [`crate::requirement::for_application`] reads
9//! it directly via `macho-codesign`. Without it, supply the bytes yourself —
10//! `csreq -b` writes the same blob — and use [`crate::requirement::from_blob`].
11
12use crate::acl::TrustedApplication;
13use crate::error::{Error, Result};
14
15/// Trust an application whose requirement blob you already have.
16///
17/// The blob must start with the `CSMAGIC_REQUIREMENT` magic (`0xfade0c00`); a
18/// requirement in any other encoding would be silently ignored by macOS.
19pub 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/// Trust the application at `path`, reading its designated requirement from its
41/// code signature.
42#[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    // A universal binary's slices share one signing identity, so the first
51    // image's requirement is the one macOS records.
52    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/// Without the `trust-apps` feature there is no way to read a signature.
70#[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    /// The designated requirement of `/usr/bin/security`, as macOS stores it in
84    /// both the binary and the ACL.
85    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        // A truncated blob declares more than it carries.
110        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}