Skip to main content

isideload_apple_codesign/
signing.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! High level signing primitives.
6
7use {
8    crate::{
9        bundle_signing::BundleSigner,
10        dmg::DmgSigner,
11        error::AppleCodesignError,
12        macho_signing::{MachOSigner, write_macho_file},
13        reader::PathType,
14        signing_settings::{SettingsScope, SigningSettings},
15    },
16    apple_xar::{reader::XarReader, signing::XarSigner},
17    log::{info, warn},
18    std::{fs::File, path::Path},
19};
20
21/// An entity for performing signing that is able to handle all supported target types.
22pub struct UnifiedSigner<'key> {
23    settings: SigningSettings<'key>,
24}
25
26impl<'key> UnifiedSigner<'key> {
27    /// Construct a new instance bound to a [SigningSettings].
28    pub fn new(settings: SigningSettings<'key>) -> Self {
29        Self { settings }
30    }
31
32    /// Signs `input_path` and writes the signed output to `output_path`.
33    pub fn sign_path(
34        &self,
35        input_path: impl AsRef<Path>,
36        output_path: impl AsRef<Path>,
37    ) -> Result<(), AppleCodesignError> {
38        let input_path = input_path.as_ref();
39
40        match PathType::from_path(input_path)? {
41            PathType::Bundle => self.sign_bundle(input_path, output_path),
42            PathType::Dmg => self.sign_dmg(input_path, output_path),
43            PathType::MachO => self.sign_macho(input_path, output_path),
44            PathType::Xar => self.sign_xar(input_path, output_path),
45            PathType::Zip | PathType::Other => Err(AppleCodesignError::UnrecognizedPathType),
46        }
47    }
48
49    /// Sign a filesystem path in place.
50    ///
51    /// This is just a convenience wrapper for [Self::sign_path()] with the same path passed
52    /// to both the input and output path.
53    pub fn sign_path_in_place(&self, path: impl AsRef<Path>) -> Result<(), AppleCodesignError> {
54        let path = path.as_ref();
55
56        self.sign_path(path, path)
57    }
58
59    /// Sign a Mach-O binary.
60    pub fn sign_macho(
61        &self,
62        input_path: impl AsRef<Path>,
63        output_path: impl AsRef<Path>,
64    ) -> Result<(), AppleCodesignError> {
65        let input_path = input_path.as_ref();
66        let output_path = output_path.as_ref();
67
68        warn!("signing {} as a Mach-O binary", input_path.display());
69
70        #[cfg(unix)]
71        {
72            use std::os::unix::fs::PermissionsExt;
73            let mut perms = std::fs::metadata(input_path)?.permissions();
74            perms.set_mode(0o755);
75            std::fs::set_permissions(input_path, perms)?;
76        }
77
78        let macho_data = std::fs::read(input_path)?;
79
80        let mut settings = self.settings.clone();
81
82        settings.import_settings_from_macho(&macho_data)?;
83
84        if settings.binary_identifier(SettingsScope::Main).is_none() {
85            let identifier = path_identifier(input_path)?;
86
87            warn!("setting binary identifier to {}", identifier);
88            settings.set_binary_identifier(SettingsScope::Main, identifier);
89        }
90
91        warn!("parsing Mach-O");
92        let signer = MachOSigner::new(&macho_data)?;
93
94        let mut macho_data = vec![];
95        signer.write_signed_binary(&settings, &mut macho_data)?;
96        warn!("writing Mach-O to {}", output_path.display());
97        write_macho_file(input_path, output_path, &macho_data)?;
98
99        Ok(())
100    }
101
102    /// Sign a `.dmg` file.
103    pub fn sign_dmg(
104        &self,
105        input_path: impl AsRef<Path>,
106        output_path: impl AsRef<Path>,
107    ) -> Result<(), AppleCodesignError> {
108        let input_path = input_path.as_ref();
109        let output_path = output_path.as_ref();
110
111        warn!("signing {} as a DMG", input_path.display());
112
113        // There must be a binary identifier on the DMG. So try to derive one
114        // from the filename if one isn't present in the settings.
115        let mut settings = self.settings.clone();
116
117        if settings.binary_identifier(SettingsScope::Main).is_none() {
118            let file_name = input_path
119                .file_stem()
120                .ok_or_else(|| {
121                    AppleCodesignError::CliGeneralError("unable to resolve file name of DMG".into())
122                })?
123                .to_string_lossy();
124
125            warn!(
126                "setting binary identifier to {} (derived from file name)",
127                file_name
128            );
129            settings.set_binary_identifier(SettingsScope::Main, file_name);
130        }
131
132        // The DMG signer signs in place because it needs a `File` handle. So if
133        // the output path is different, copy the DMG first.
134
135        // This is not robust same file detection.
136        if input_path != output_path {
137            info!(
138                "copying {} to {} in preparation for signing",
139                input_path.display(),
140                output_path.display()
141            );
142            if let Some(parent) = output_path.parent() {
143                std::fs::create_dir_all(parent)?;
144            }
145
146            std::fs::copy(input_path, output_path)?;
147        }
148
149        let signer = DmgSigner::default();
150        let mut fh = std::fs::File::options()
151            .read(true)
152            .write(true)
153            .open(output_path)?;
154        signer.sign_file(&settings, &mut fh)?;
155
156        Ok(())
157    }
158
159    /// Sign a bundle.
160    pub fn sign_bundle(
161        &self,
162        input_path: impl AsRef<Path>,
163        output_path: impl AsRef<Path>,
164    ) -> Result<(), AppleCodesignError> {
165        let input_path = input_path.as_ref();
166        warn!("signing bundle at {}", input_path.display());
167
168        let mut signer = BundleSigner::new_from_path(input_path)?;
169        signer.collect_nested_bundles()?;
170        signer.write_signed_bundle(output_path, &self.settings)?;
171
172        Ok(())
173    }
174
175    pub fn sign_xar(
176        &self,
177        input_path: impl AsRef<Path>,
178        output_path: impl AsRef<Path>,
179    ) -> Result<(), AppleCodesignError> {
180        let input_path = input_path.as_ref();
181        let output_path = output_path.as_ref();
182
183        // The XAR can get corrupted if we sign into place. So we always go through a temporary
184        // file. We could potentially avoid the overhead if we're not signing in place...
185
186        let output_path_temp =
187            output_path.with_file_name(if let Some(file_name) = output_path.file_name() {
188                file_name.to_string_lossy().to_string() + ".tmp"
189            } else {
190                "xar.tmp".to_string()
191            });
192
193        warn!(
194            "signing XAR pkg installer at {} to {}",
195            input_path.display(),
196            output_path_temp.display()
197        );
198
199        let (signing_key, signing_cert) = self
200            .settings
201            .signing_key()
202            .ok_or(AppleCodesignError::XarNoAdhoc)?;
203
204        {
205            let reader = XarReader::new(File::open(input_path)?)?;
206            let mut signer = XarSigner::new(reader);
207
208            let mut fh = File::create(&output_path_temp)?;
209            signer.sign(
210                &mut fh,
211                signing_key,
212                signing_cert,
213                self.settings.certificate_chain().iter().cloned(),
214            )?;
215        }
216
217        if output_path.exists() {
218            warn!("removing existing {}", output_path.display());
219            std::fs::remove_file(output_path)?;
220        }
221
222        warn!(
223            "renaming {} -> {}",
224            output_path_temp.display(),
225            output_path.display()
226        );
227        std::fs::rename(&output_path_temp, output_path)?;
228
229        Ok(())
230    }
231}
232
233pub fn path_identifier(path: impl AsRef<Path>) -> Result<String, AppleCodesignError> {
234    let path = path.as_ref();
235
236    // We only care about the file name.
237    let file_name = path
238        .file_name()
239        .ok_or_else(|| {
240            AppleCodesignError::PathIdentifier(format!("path {} lacks a file name", path.display()))
241        })?
242        .to_string_lossy()
243        .to_string();
244
245    // Remove the final file extension unless it is numeric.
246    let id = if let Some((prefix, extension)) = file_name.rsplit_once('.') {
247        if extension.chars().all(|c| c.is_ascii_digit()) {
248            file_name.as_str()
249        } else {
250            prefix
251        }
252    } else {
253        file_name.as_str()
254    };
255
256    let is_digit_or_dot = |c: char| c == '.' || c.is_ascii_digit();
257
258    // If begins with digit or dot, use as is, handling empty string special
259    // case.
260    let id = match id.chars().next() {
261        Some(first) => {
262            if is_digit_or_dot(first) {
263                return Ok(id.to_string());
264            } else {
265                id
266            }
267        }
268        None => {
269            return Ok(id.to_string());
270        }
271    };
272
273    // Strip all components having numeric *suffixes* except the first
274    // one. This doesn't strip extension components but *suffixes*. So
275    // e.g. libFoo1.2.3 -> libFoo1. Logically, we strip trailing digits
276    // + dot after the first dot preceded by digits.
277
278    let prefix = id.trim_end_matches(is_digit_or_dot);
279    let stripped = &id[prefix.len()..];
280
281    if stripped.is_empty() {
282        Ok(id.to_string())
283    } else {
284        // If the next character is a dot, add it back in.
285        let (prefix, stripped) = if matches!(stripped.chars().next(), Some('.')) {
286            (&id[0..prefix.len() + 1], &stripped[1..])
287        } else {
288            (prefix, stripped)
289        };
290
291        // Add back in any leading digits.
292
293        let id = prefix
294            .chars()
295            .chain(stripped.chars().take_while(|c| c.is_ascii_digit()))
296            .collect::<String>();
297
298        Ok(id)
299    }
300}
301
302#[cfg(test)]
303mod test {
304    use super::*;
305    #[test]
306    fn path_identifier_normalization() {
307        assert_eq!(path_identifier("foo").unwrap(), "foo");
308        assert_eq!(path_identifier("foo.dylib").unwrap(), "foo");
309        assert_eq!(path_identifier("/etc/foo.dylib").unwrap(), "foo");
310        assert_eq!(path_identifier("/etc/foo").unwrap(), "foo");
311
312        // Starts with digit or dot is preserved module final extension.
313        assert_eq!(path_identifier(".foo").unwrap(), "");
314        assert_eq!(path_identifier("123").unwrap(), "123");
315        assert_eq!(path_identifier(".foo.dylib").unwrap(), ".foo");
316        assert_eq!(path_identifier("123.dylib").unwrap(), "123");
317        assert_eq!(path_identifier("123.42").unwrap(), "123.42");
318
319        // Digit final extension preserved.
320
321        assert_eq!(path_identifier("foo1").unwrap(), "foo1");
322        assert_eq!(path_identifier("foo1.dylib").unwrap(), "foo1");
323        assert_eq!(path_identifier("foo1.2.dylib").unwrap(), "foo1");
324        assert_eq!(path_identifier("foo1.2").unwrap(), "foo1");
325        assert_eq!(path_identifier("foo1.2.3.4.dylib").unwrap(), "foo1");
326        assert_eq!(path_identifier("foo.1").unwrap(), "foo.1");
327        assert_eq!(path_identifier("foo.1.2.3").unwrap(), "foo.1");
328        assert_eq!(path_identifier("foo.1.2.dylib").unwrap(), "foo.1");
329        assert_eq!(path_identifier("foo.1.dylib").unwrap(), "foo.1");
330    }
331}