Skip to main content

security/code_signing/
mod.rs

1//! Current-process code-signing inspection.
2
3use std::collections::BTreeMap;
4
5use apple_cf::CFError;
6
7use crate::error::{Result, SecurityError};
8use crate::ffi;
9use crate::private::{
10    cf_boolean_to_bool, cf_data_to_vec, cf_dictionary_entries, cf_dictionary_get_value,
11    cf_number_to_i64, cf_string_to_string, cf_type_id, sec_error_message, OwnedCf,
12};
13
14/// Simplified snapshot of a Core Foundation value embedded in signing metadata.
15#[derive(Debug, Clone, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum SigningValue {
18    Boolean(bool),
19    Integer(i64),
20    String(String),
21    Data(Vec<u8>),
22    Array(Vec<Self>),
23    Dictionary(BTreeMap<String, Self>),
24    Unknown(String),
25}
26
27/// High-level view of `SecCodeCopySigningInformation` for the current process.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct SigningInformation {
30    /// Bundle identifier or executable identifier if the process is signed.
31    pub identifier: Option<String>,
32    /// Team identifier, when the signature contains one.
33    pub team_identifier: Option<String>,
34    /// Embedded entitlements in dictionary form.
35    pub entitlements: BTreeMap<String, SigningValue>,
36    /// Whether the entitlements indicate App Sandbox is enabled.
37    pub sandboxed: bool,
38    /// Dynamic status word from the code-signing subsystem.
39    pub status: Option<u32>,
40}
41
42impl SigningInformation {
43    /// Whether the process appears to be signed.
44    #[must_use]
45    pub const fn is_signed(&self) -> bool {
46        self.identifier.is_some()
47    }
48}
49
50/// Owned `SecCodeRef` for a running process.
51pub struct Code {
52    raw: ffi::SecCodeRef,
53}
54
55impl Code {
56    /// Capture the current process as a `SecCodeRef`.
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if Security.framework cannot create the code object.
61    pub fn current() -> Result<Self> {
62        let mut raw = std::ptr::null();
63        let status = unsafe { ffi::SecCodeCopySelf(ffi::kSecCSDefaultFlags, &mut raw) };
64        if status != ffi::status::SUCCESS {
65            return Err(SecurityError::from_status(
66                "SecCodeCopySelf",
67                status,
68                sec_error_message(status),
69            ));
70        }
71        if raw.is_null() {
72            return Err(SecurityError::CoreFoundation(CFError::new(
73                "SecCodeCopySelf",
74            )));
75        }
76        Ok(Self { raw })
77    }
78
79    /// Fetch signing information for this code object.
80    ///
81    /// # Errors
82    ///
83    /// Returns an error if Security.framework rejects the query or returns an unexpected type.
84    pub fn signing_information(&self) -> Result<SigningInformation> {
85        let mut info = std::ptr::null();
86        let flags = ffi::kSecCSSigningInformation | ffi::kSecCSDynamicInformation;
87        let status =
88            unsafe { ffi::SecCodeCopySigningInformation(self.raw.cast(), flags, &mut info) };
89        if status != ffi::status::SUCCESS {
90            return Err(SecurityError::from_status(
91                "SecCodeCopySigningInformation",
92                status,
93                sec_error_message(status),
94            ));
95        }
96        if info.is_null() {
97            return Err(SecurityError::CoreFoundation(CFError::new(
98                "SecCodeCopySigningInformation",
99            )));
100        }
101        let info = OwnedCf::new(info.cast());
102        if cf_type_id(info.as_ptr()) != unsafe { ffi::CFDictionaryGetTypeID() } {
103            return Err(SecurityError::UnexpectedType {
104                operation: "SecCodeCopySigningInformation",
105                expected: "CFDictionary",
106            });
107        }
108
109        let identifier = string_value(info.as_dictionary(), unsafe { ffi::kSecCodeInfoIdentifier });
110        let team_identifier =
111            string_value(info.as_dictionary(), unsafe { ffi::kSecCodeInfoTeamIdentifier });
112        let entitlements =
113            dictionary_value(info.as_dictionary(), unsafe { ffi::kSecCodeInfoEntitlementsDict })
114                .map(cf_dictionary_to_map)
115                .unwrap_or_default();
116        let sandboxed = matches!(
117            entitlements.get("com.apple.security.app-sandbox"),
118            Some(SigningValue::Boolean(true))
119        );
120        let status = cf_dictionary_get_value(info.as_dictionary(), unsafe { ffi::kSecCodeInfoStatus });
121        let status = cf_number_to_i64(status.cast()).and_then(|value| u32::try_from(value).ok());
122
123        Ok(SigningInformation {
124            identifier,
125            team_identifier,
126            entitlements,
127            sandboxed,
128            status,
129        })
130    }
131
132    /// Borrow the raw `SecCodeRef`.
133    #[must_use]
134    pub const fn as_raw(&self) -> ffi::SecCodeRef {
135        self.raw
136    }
137}
138
139impl Drop for Code {
140    fn drop(&mut self) {
141        if !self.raw.is_null() {
142            unsafe { ffi::CFRelease(self.raw.cast()) };
143        }
144    }
145}
146
147impl core::fmt::Debug for Code {
148    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
149        f.debug_struct("Code").field("raw", &self.raw).finish()
150    }
151}
152
153fn string_value(dictionary: ffi::CFDictionaryRef, key: ffi::CFStringRef) -> Option<String> {
154    let value = cf_dictionary_get_value(dictionary, key);
155    if value.is_null() || cf_type_id(value) != unsafe { ffi::CFStringGetTypeID() } {
156        return None;
157    }
158    cf_string_to_string(value.cast())
159}
160
161fn dictionary_value(
162    dictionary: ffi::CFDictionaryRef,
163    key: ffi::CFStringRef,
164) -> Option<ffi::CFDictionaryRef> {
165    let value = cf_dictionary_get_value(dictionary, key);
166    if value.is_null() || cf_type_id(value) != unsafe { ffi::CFDictionaryGetTypeID() } {
167        return None;
168    }
169    Some(value.cast())
170}
171
172fn cf_dictionary_to_map(dictionary: ffi::CFDictionaryRef) -> BTreeMap<String, SigningValue> {
173    cf_dictionary_entries(dictionary)
174        .into_iter()
175        .filter_map(|(key, value)| {
176            if key.is_null() || cf_type_id(key) != unsafe { ffi::CFStringGetTypeID() } {
177                return None;
178            }
179            let key = cf_string_to_string(key.cast())?;
180            Some((key, cf_type_to_value(value.cast())))
181        })
182        .collect()
183}
184
185fn cf_type_to_value(value: ffi::CFTypeRef) -> SigningValue {
186    if value.is_null() {
187        return SigningValue::Unknown("null".to_owned());
188    }
189
190    let type_id = cf_type_id(value);
191    if type_id == unsafe { ffi::CFBooleanGetTypeID() } {
192        return SigningValue::Boolean(cf_boolean_to_bool(value.cast()));
193    }
194    if type_id == unsafe { ffi::CFNumberGetTypeID() } {
195        return cf_number_to_i64(value.cast()).map_or_else(
196            || SigningValue::Unknown("number".to_owned()),
197            SigningValue::Integer,
198        );
199    }
200    if type_id == unsafe { ffi::CFStringGetTypeID() } {
201        return cf_string_to_string(value.cast()).map_or_else(
202            || SigningValue::Unknown("string".to_owned()),
203            SigningValue::String,
204        );
205    }
206    if type_id == unsafe { ffi::CFDataGetTypeID() } {
207        return SigningValue::Data(cf_data_to_vec(value.cast()));
208    }
209    if type_id == unsafe { ffi::CFArrayGetTypeID() } {
210        let count = unsafe { ffi::CFArrayGetCount(value.cast()) };
211        let count = usize::try_from(count).unwrap_or_default();
212        let values = (0..count)
213            .filter_map(|index| isize::try_from(index).ok())
214            .map(|index| unsafe { ffi::CFArrayGetValueAtIndex(value.cast(), index) })
215            .filter(|value| !value.is_null())
216            .map(|value| cf_type_to_value(value.cast()))
217            .collect();
218        return SigningValue::Array(values);
219    }
220    if type_id == unsafe { ffi::CFDictionaryGetTypeID() } {
221        return SigningValue::Dictionary(cf_dictionary_to_map(value.cast()));
222    }
223
224    SigningValue::Unknown(format!("CFTypeID({type_id})"))
225}