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::{write_macho_file, MachOSigner},
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.time_stamp_url(),
214                self.settings.certificate_chain().iter().cloned(),
215            )?;
216        }
217
218        if output_path.exists() {
219            warn!("removing existing {}", output_path.display());
220            std::fs::remove_file(output_path)?;
221        }
222
223        warn!(
224            "renaming {} -> {}",
225            output_path_temp.display(),
226            output_path.display()
227        );
228        std::fs::rename(&output_path_temp, output_path)?;
229
230        Ok(())
231    }
232}
233
234pub fn path_identifier(path: impl AsRef<Path>) -> Result<String, AppleCodesignError> {
235    let path = path.as_ref();
236
237    // We only care about the file name.
238    let file_name = path
239        .file_name()
240        .ok_or_else(|| {
241            AppleCodesignError::PathIdentifier(format!("path {} lacks a file name", path.display()))
242        })?
243        .to_string_lossy()
244        .to_string();
245
246    // Remove the final file extension unless it is numeric.
247    let id = if let Some((prefix, extension)) = file_name.rsplit_once('.') {
248        if extension.chars().all(|c| c.is_ascii_digit()) {
249            file_name.as_str()
250        } else {
251            prefix
252        }
253    } else {
254        file_name.as_str()
255    };
256
257    let is_digit_or_dot = |c: char| c == '.' || c.is_ascii_digit();
258
259    // If begins with digit or dot, use as is, handling empty string special
260    // case.
261    let id = match id.chars().next() {
262        Some(first) => {
263            if is_digit_or_dot(first) {
264                return Ok(id.to_string());
265            } else {
266                id
267            }
268        }
269        None => {
270            return Ok(id.to_string());
271        }
272    };
273
274    // Strip all components having numeric *suffixes* except the first
275    // one. This doesn't strip extension components but *suffixes*. So
276    // e.g. libFoo1.2.3 -> libFoo1. Logically, we strip trailing digits
277    // + dot after the first dot preceded by digits.
278
279    let prefix = id.trim_end_matches(is_digit_or_dot);
280    let stripped = &id[prefix.len()..];
281
282    if stripped.is_empty() {
283        Ok(id.to_string())
284    } else {
285        // If the next character is a dot, add it back in.
286        let (prefix, stripped) = if matches!(stripped.chars().next(), Some('.')) {
287            (&id[0..prefix.len() + 1], &stripped[1..])
288        } else {
289            (prefix, stripped)
290        };
291
292        // Add back in any leading digits.
293
294        let id = prefix
295            .chars()
296            .chain(stripped.chars().take_while(|c| c.is_ascii_digit()))
297            .collect::<String>();
298
299        Ok(id)
300    }
301}
302
303#[cfg(test)]
304mod test {
305    use super::*;
306    #[test]
307    fn path_identifier_normalization() {
308        assert_eq!(path_identifier("foo").unwrap(), "foo");
309        assert_eq!(path_identifier("foo.dylib").unwrap(), "foo");
310        assert_eq!(path_identifier("/etc/foo.dylib").unwrap(), "foo");
311        assert_eq!(path_identifier("/etc/foo").unwrap(), "foo");
312
313        // Starts with digit or dot is preserved module final extension.
314        assert_eq!(path_identifier(".foo").unwrap(), "");
315        assert_eq!(path_identifier("123").unwrap(), "123");
316        assert_eq!(path_identifier(".foo.dylib").unwrap(), ".foo");
317        assert_eq!(path_identifier("123.dylib").unwrap(), "123");
318        assert_eq!(path_identifier("123.42").unwrap(), "123.42");
319
320        // Digit final extension preserved.
321
322        assert_eq!(path_identifier("foo1").unwrap(), "foo1");
323        assert_eq!(path_identifier("foo1.dylib").unwrap(), "foo1");
324        assert_eq!(path_identifier("foo1.2.dylib").unwrap(), "foo1");
325        assert_eq!(path_identifier("foo1.2").unwrap(), "foo1");
326        assert_eq!(path_identifier("foo1.2.3.4.dylib").unwrap(), "foo1");
327        assert_eq!(path_identifier("foo.1").unwrap(), "foo.1");
328        assert_eq!(path_identifier("foo.1.2.3").unwrap(), "foo.1");
329        assert_eq!(path_identifier("foo.1.2.dylib").unwrap(), "foo.1");
330        assert_eq!(path_identifier("foo.1.dylib").unwrap(), "foo.1");
331    }
332}