Skip to main content

isideload_apple_codesign/
bundle_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//! Functionality for signing Apple bundles.
6
7use {
8    crate::{
9        code_directory::CodeDirectoryBlob,
10        code_requirement::{CodeRequirementExpression, RequirementType},
11        code_resources::{normalized_resources_path, CodeResourcesBuilder, CodeResourcesRule},
12        cryptography::DigestType,
13        embedded_signature::{Blob, BlobData},
14        error::AppleCodesignError,
15        macho::MachFile,
16        macho_signing::{write_macho_file, MachOSigner},
17        signing::path_identifier,
18        signing_settings::{SettingsScope, SigningSettings},
19    },
20    apple_bundles::{BundlePackageType, DirectoryBundle},
21    log::{debug, info, warn},
22    simple_file_manifest::create_symlink,
23    std::{
24        borrow::Cow,
25        collections::{BTreeMap, BTreeSet},
26        io::Write,
27        path::{Path, PathBuf},
28    },
29};
30
31/// Copy a bundle's contents to a destination directory.
32///
33/// This does not use the CodeResources rules for a bundle. Rather, it
34/// blindly copies all files in the bundle. This means that excluded files
35/// can be copied.
36///
37/// Returns the set of bundle-relative paths that are installed.
38pub fn copy_bundle(
39    bundle: &DirectoryBundle,
40    dest_dir: &Path,
41) -> Result<BTreeSet<PathBuf>, AppleCodesignError> {
42    let settings = SigningSettings::default();
43
44    let mut context = BundleSigningContext {
45        dest_dir: dest_dir.to_path_buf(),
46        settings: &settings,
47        previously_installed_paths: Default::default(),
48        installed_paths: Default::default(),
49    };
50
51    for file in bundle
52        .files(false)
53        .map_err(AppleCodesignError::DirectoryBundle)?
54    {
55        context.install_file(file.absolute_path(), file.relative_path())?;
56    }
57
58    Ok(context.installed_paths)
59}
60
61/// A primitive for signing an Apple bundle.
62///
63/// This type handles the high-level logic of signing an Apple bundle (e.g.
64/// a `.app` or `.framework` directory with a well-defined structure).
65///
66/// This type handles the signing of nested bundles (if present) such that
67/// they chain to the main bundle's signature.
68pub struct BundleSigner {
69    /// All the bundles being signed, indexed by relative path.
70    bundles: BTreeMap<Option<String>, SingleBundleSigner>,
71}
72
73impl BundleSigner {
74    /// Construct a new instance given the path to an on-disk bundle.
75    ///
76    /// The path should be the root directory of the bundle. e.g. `MyApp.app`.
77    pub fn new_from_path(path: impl AsRef<Path>) -> Result<Self, AppleCodesignError> {
78        let main_bundle = DirectoryBundle::new_from_path(path.as_ref())
79            .map_err(AppleCodesignError::DirectoryBundle)?;
80        let root_bundle_path = main_bundle.root_dir().to_path_buf();
81
82        let mut bundles = BTreeMap::default();
83
84        bundles.insert(None, SingleBundleSigner::new(root_bundle_path, main_bundle));
85
86        Ok(Self { bundles })
87    }
88
89    /// Find bundles in subdirectories of the main bundle and mark them for signing.
90    pub fn collect_nested_bundles(&mut self) -> Result<(), AppleCodesignError> {
91        let (root_bundle_path, nested) = {
92            let main = self.bundles.get(&None).expect("main bundle should exist");
93
94            let nested = main
95                .bundle
96                .nested_bundles(true)
97                .map_err(AppleCodesignError::DirectoryBundle)?;
98
99            (main.root_bundle_path.clone(), nested)
100        };
101
102        self.bundles.extend(
103        nested.into_iter()
104            .filter(|(k, bundle)| {
105                // Our bundle classifier is very aggressive about annotating directories
106                // as bundles. Pretty much anything with an Info.plist can get through.
107                // We apply additional filtering here so we only emit bundles that can
108                // be signed.
109                //
110                // A better solution here is to use the CodeResources rule
111                // based file walker to look for directories with the "nested" flag.
112                // If a bundle-looking directory exists outside of a "nested" rule,
113                // it probably shouldn't be signed.
114
115                let (has_package_type, has_signable_package_type) = if let Ok(Some(pt)) = bundle.info_plist_key_string("CFBundlePackageType") {
116                    (true, pt != "dSYM")
117                } else {
118                    (false, false)
119                };
120
121                let has_bundle_identifier = matches!(bundle.info_plist_key_string("CFBundleIdentifier"), Ok(Some(_)));
122
123                match (has_package_type, has_signable_package_type, has_bundle_identifier)  {
124                    (true, false, _) =>{
125                        debug!("{k} discarded because its CFBundlePackageType is not signable");
126                        false
127                    }
128                    (true, _, true) => {
129                        // It quacks like a bundle.
130                        true
131                    }
132                    (false, _, false) => {
133                        // This looks like a naked Info.plist.
134                        debug!("{k} discarded as a signable bundle because its Info.plist lacks CFBundlePackageType and CFBundleIdentifier");
135                        false
136                    }
137                    (true, _, false) => {
138                        info!("{k} has an Info.plist with a CFBundlePackageType but not a CFBundleIdentifier; we'll try to sign it but we recommend adding a CFBundleIdentifier");
139                        true
140                    }
141                    (false, _, true) => {
142                        info!("{k} has an Info.plist with a CFBundleIdentifier but without a CFBundlePackageType; we'll try to sign it but we recommend adding a CFBundlePackageType");
143                        true
144                    }
145                }
146            })
147            .map(|(k, bundle)| {
148                (
149                    Some(k),
150                    SingleBundleSigner::new(root_bundle_path.clone(), bundle),
151                )
152            })
153        );
154
155        Ok(())
156    }
157
158    /// Write a signed bundle to the given destination directory.
159    ///
160    /// The destination directory can be the same as the source directory. However,
161    /// if this is done and an error occurs in the middle of signing, the bundle
162    /// may be left in an inconsistent or corrupted state and may not be usable.
163    pub fn write_signed_bundle(
164        &self,
165        dest_dir: impl AsRef<Path>,
166        settings: &SigningSettings,
167    ) -> Result<DirectoryBundle, AppleCodesignError> {
168        let dest_dir = dest_dir.as_ref();
169
170        // We need to sign the leaf-most bundles first since a parent bundle may need
171        // to record information about the child in its signature.
172        let mut bundles = self
173            .bundles
174            .iter()
175            .filter_map(|(rel, bundle)| rel.as_ref().map(|rel| (rel, bundle)))
176            .collect::<Vec<_>>();
177
178        // This won't preserve alphabetical order. But since the input was stable, output
179        // should be deterministic.
180        bundles.sort_by(|(a, _), (b, _)| b.len().cmp(&a.len()));
181
182        if !bundles.is_empty() {
183            if settings.shallow() {
184                warn!("{} nested bundles will be copied instead of signed because shallow signing enabled:", bundles.len());
185            } else {
186                warn!(
187                    "signing {} nested bundles in the following order:",
188                    bundles.len()
189                );
190            }
191            for bundle in &bundles {
192                warn!("{}", bundle.0);
193            }
194        }
195
196        // We keep track of root relative input paths that have been installed so we can
197        // skip installing in case we encounter the path again when signing a parent
198        // bundle.
199        //
200        // If we fail to do this, during non-shallow signing operations we may descend
201        // into a child bundle that is outside a directory with the "nested" flag set.
202        // Files in non-"nested" directories need to be sealed in CodeResources files
203        // as regular files, not bundles. So we need to walk into the child bundle in
204        // this scenario. But during the walk we want to prevent already installed files
205        // from being processed again.
206        //
207        // In the case of Mach-O binaries in the above non-"nested" directory scenario,
208        // excluding already signed files prevents the Mach-O from being signed again.
209        // Signing the Mach-O twice could invalidate the bundle's signature and/or result
210        // in incorrect signing settings since a bundle's main binary wouldn't be
211        // recognized as such since we're outside the context of that bundle.
212        //
213        // In all cases, we prevent redundant work installing files if a file is seen
214        // twice.
215        let mut installed_rel_paths = BTreeSet::<PathBuf>::new();
216
217        for (rel, nested) in bundles {
218            let rel_path = PathBuf::from(rel);
219
220            let nested_dest_dir = dest_dir.join(rel);
221            warn!("entering nested bundle {}", rel,);
222
223            let bundle_installed_rel_paths = if settings.shallow() {
224                warn!("shallow signing enabled; bundle will be copied instead of signed");
225                copy_bundle(&nested.bundle, &nested_dest_dir)?
226            } else if settings.path_exclusion_pattern_matches(rel) {
227                // If we excluded this bundle from signing, just copy all the files.
228                warn!("bundle is in exclusion list; it will be copied instead of signed");
229                copy_bundle(&nested.bundle, &nested_dest_dir)?
230            } else {
231                let bundle_installed = installed_rel_paths
232                    .iter()
233                    .filter_map(|p| {
234                        if let Ok(p) = p.strip_prefix(&rel_path) {
235                            Some(p.to_path_buf())
236                        } else {
237                            None
238                        }
239                    })
240                    .collect::<BTreeSet<_>>();
241
242                let info = nested.write_signed_bundle(
243                    nested_dest_dir,
244                    &settings.as_nested_bundle_settings(rel),
245                    bundle_installed,
246                )?;
247
248                info.installed_rel_paths
249            };
250
251            for p in bundle_installed_rel_paths {
252                installed_rel_paths.insert(rel_path.join(p).to_path_buf());
253            }
254
255            warn!("leaving nested bundle {}", rel);
256        }
257
258        let main = self
259            .bundles
260            .get(&None)
261            .expect("main bundle should have a key");
262
263        Ok(main
264            .write_signed_bundle(dest_dir, settings, installed_rel_paths)?
265            .bundle)
266    }
267}
268
269/// Metadata about a signed Mach-O file or bundle.
270///
271/// If referring to a bundle, the metadata refers to the 1st Mach-O in the
272/// bundle's main executable.
273///
274/// This contains enough metadata to construct references to the file/bundle
275/// in [crate::code_resources::CodeResources] files.
276pub struct SignedMachOInfo {
277    /// Raw data constituting the code directory blob.
278    ///
279    /// Is typically digested to construct a <cdhash>.
280    pub code_directory_blob: Vec<u8>,
281
282    /// Designated code requirements string.
283    ///
284    /// Typically occupies a `<key>requirement</key>` in a
285    /// [crate::code_resources::CodeResources] file.
286    pub designated_code_requirement: Option<String>,
287}
288
289impl SignedMachOInfo {
290    /// Parse Mach-O data to obtain an instance.
291    pub fn parse_data(data: &[u8]) -> Result<Self, AppleCodesignError> {
292        // Initial Mach-O's signature data is used.
293        let mach = MachFile::parse(data)?;
294        let macho = mach.nth_macho(0)?;
295
296        let signature = macho
297            .code_signature()?
298            .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
299
300        let code_directory_blob = signature.preferred_code_directory()?.to_blob_bytes()?;
301
302        let designated_code_requirement = if let Some(requirements) =
303            signature.code_requirements()?
304        {
305            if let Some(designated) = requirements.requirements.get(&RequirementType::Designated) {
306                let req = designated.parse_expressions()?;
307
308                Some(format!("{}", req[0]))
309            } else {
310                // In case no explicit requirements has been set, we use current file cdhashes.
311                let mut requirement_expr = None;
312
313                // We record the 20 byte digests of every code directory in every
314                // Mach-O.
315                // Note: Apple's tooling appears to always record the x86-64 Mach-O
316                // first, even if it isn't first in the universal binary. Since we're
317                // dealing with a bunch of OR'd code requirements expressions, we don't
318                // believe this difference is worth caring about.
319                for macho in mach.iter_macho() {
320                    for (_, cd) in macho
321                        .code_signature()?
322                        .ok_or(AppleCodesignError::BinaryNoCodeSignature)?
323                        .all_code_directories()?
324                    {
325                        let digest_type = if cd.digest_type == DigestType::Sha256 {
326                            DigestType::Sha256Truncated
327                        } else {
328                            cd.digest_type
329                        };
330
331                        let digest = digest_type.digest_data(&cd.to_blob_bytes()?)?;
332                        let expression = Box::new(CodeRequirementExpression::CodeDirectoryHash(
333                            Cow::from(digest),
334                        ));
335
336                        if let Some(left_part) = requirement_expr {
337                            requirement_expr = Some(Box::new(CodeRequirementExpression::Or(
338                                left_part, expression,
339                            )))
340                        } else {
341                            requirement_expr = Some(expression);
342                        }
343                    }
344                }
345
346                Some(format!(
347                    "{}",
348                    requirement_expr.expect("a Mach-O should have been present")
349                ))
350            }
351        } else {
352            None
353        };
354
355        Ok(SignedMachOInfo {
356            code_directory_blob,
357            designated_code_requirement,
358        })
359    }
360
361    /// Resolve the parsed code directory from stored data.
362    pub fn code_directory(&self) -> Result<Box<CodeDirectoryBlob<'_>>, AppleCodesignError> {
363        let blob = BlobData::from_blob_bytes(&self.code_directory_blob)?;
364
365        if let BlobData::CodeDirectory(cd) = blob {
366            Ok(cd)
367        } else {
368            Err(AppleCodesignError::BinaryNoCodeSignature)
369        }
370    }
371
372    /// Resolve the notarization ticket record name for this Mach-O file.
373    pub fn notarization_ticket_record_name(&self) -> Result<String, AppleCodesignError> {
374        let cd = self.code_directory()?;
375
376        let digest_type: u8 = cd.digest_type.into();
377
378        let mut digest = cd.digest_with(cd.digest_type)?;
379
380        // Digests appear to be truncated at 20 bytes / 40 characters.
381        digest.truncate(20);
382
383        let digest = hex::encode(digest);
384
385        // Unsure what the leading `2/` means.
386        Ok(format!("2/{digest_type}/{digest}"))
387    }
388}
389
390/// Holds state and helper methods to facilitate signing a bundle.
391pub struct BundleSigningContext<'a, 'key> {
392    /// Settings for this bundle.
393    pub settings: &'a SigningSettings<'key>,
394    /// Where the bundle is getting installed to.
395    pub dest_dir: PathBuf,
396    /// Bundle relative paths of files that have already been installed.
397    ///
398    /// The already-present destination file content should be used for sealing.
399    pub previously_installed_paths: BTreeSet<PathBuf>,
400    /// Bundle relative paths of files that are installed by this signing operation.
401    pub installed_paths: BTreeSet<PathBuf>,
402}
403
404impl<'a, 'key> BundleSigningContext<'a, 'key> {
405    /// Install a file (regular or symlink) in the destination directory.
406    pub fn install_file(
407        &mut self,
408        source_path: &Path,
409        bundle_rel_path: &Path,
410    ) -> Result<PathBuf, AppleCodesignError> {
411        let dest_path = self.dest_dir.join(bundle_rel_path);
412
413        if source_path != dest_path {
414            // Remove an existing file before installing the replacement. In
415            // the case of symlinks this is required due to how symlink creation
416            // works.
417            if dest_path.symlink_metadata().is_ok() {
418                std::fs::remove_file(&dest_path)?;
419            }
420
421            if let Some(parent) = dest_path.parent() {
422                std::fs::create_dir_all(parent)?;
423            }
424
425            let metadata = source_path.symlink_metadata()?;
426            let mtime = filetime::FileTime::from_last_modification_time(&metadata);
427
428            if metadata.file_type().is_symlink() {
429                let target = std::fs::read_link(source_path)?;
430                info!(
431                    "replicating symlink {} -> {}",
432                    dest_path.display(),
433                    target.display()
434                );
435                create_symlink(&dest_path, target)?;
436                filetime::set_symlink_file_times(
437                    &dest_path,
438                    filetime::FileTime::from_last_access_time(&metadata),
439                    mtime,
440                )?;
441            } else {
442                info!(
443                    "copying file {} -> {}",
444                    source_path.display(),
445                    dest_path.display()
446                );
447                // TODO consider stripping XATTR_RESOURCEFORK_NAME and XATTR_FINDERINFO_NAME.
448                std::fs::copy(source_path, &dest_path)?;
449                filetime::set_file_mtime(&dest_path, mtime)?;
450            }
451        }
452
453        // Always record the installation even if we no-op. The intent of the
454        // annotation is to mark files that are already present in the destination
455        // bundle.
456        self.installed_paths.insert(bundle_rel_path.to_path_buf());
457
458        Ok(dest_path)
459    }
460
461    /// Sign a Mach-O file and ensure its new content is installed.
462    ///
463    /// Returns Mach-O metadata which can be recorded in a CodeResources file.
464
465    pub fn sign_and_install_macho(
466        &mut self,
467        source_path: &Path,
468        bundle_rel_path: &Path,
469    ) -> Result<(PathBuf, SignedMachOInfo), AppleCodesignError> {
470        warn!("signing Mach-O file {}", bundle_rel_path.display());
471
472        #[cfg(unix)]
473        {
474            use std::os::unix::fs::PermissionsExt;
475            let mut perms = std::fs::metadata(source_path)?.permissions();
476            perms.set_mode(0o755);
477            std::fs::set_permissions(source_path, perms)?;
478        }
479
480        let macho_data = std::fs::read(source_path)?;
481        let signer = MachOSigner::new(&macho_data)?;
482
483        let mut settings = self
484            .settings
485            .as_bundle_macho_settings(bundle_rel_path.to_string_lossy().as_ref());
486
487        // When signing a Mach-O in the context of a bundle, always define the
488        // binary identifier from the filename so everything is consistent.
489        // Unless an existing setting overrides it, of course.
490        if settings.binary_identifier(SettingsScope::Main).is_none() {
491            let identifier = path_identifier(bundle_rel_path)?;
492            info!("setting binary identifier based on path: {}", identifier);
493
494            settings.set_binary_identifier(SettingsScope::Main, &identifier);
495        }
496
497        settings.import_settings_from_macho(&macho_data)?;
498
499        let mut new_data = Vec::<u8>::with_capacity(macho_data.len() + 2_usize.pow(17));
500        signer.write_signed_binary(&settings, &mut new_data)?;
501
502        let dest_path = self.dest_dir.join(bundle_rel_path);
503
504        info!("writing Mach-O to {}", dest_path.display());
505        write_macho_file(source_path, &dest_path, &new_data)?;
506
507        let info = SignedMachOInfo::parse_data(&new_data)?;
508
509        self.installed_paths.insert(bundle_rel_path.to_path_buf());
510
511        Ok((dest_path, info))
512    }
513}
514
515/// Holds metadata describing the result of a bundle signing operation.
516pub struct BundleSigningInfo {
517    /// The signed bundle.
518    pub bundle: DirectoryBundle,
519
520    /// Bundle relative paths of files that are installed by this signing operation.
521    pub installed_rel_paths: BTreeSet<PathBuf>,
522}
523
524/// A primitive for signing a single Apple bundle.
525///
526/// Unlike [BundleSigner], this type only signs a single bundle and is ignorant
527/// about nested bundles. You probably want to use [BundleSigner] as the interface
528/// for signing bundles, as failure to account for nested bundles can result in
529/// signature verification errors.
530pub struct SingleBundleSigner {
531    /// Path of the root bundle being signed.
532    root_bundle_path: PathBuf,
533
534    /// The bundle being signed.
535    bundle: DirectoryBundle,
536}
537
538impl SingleBundleSigner {
539    /// Construct a new instance.
540    pub fn new(root_bundle_path: PathBuf, bundle: DirectoryBundle) -> Self {
541        Self {
542            root_bundle_path,
543            bundle,
544        }
545    }
546
547    /// Write a signed bundle to the given directory.
548    pub fn write_signed_bundle(
549        &self,
550        dest_dir: impl AsRef<Path>,
551        settings: &SigningSettings,
552        previously_installed_paths: BTreeSet<PathBuf>,
553    ) -> Result<BundleSigningInfo, AppleCodesignError> {
554        let dest_dir = dest_dir.as_ref();
555
556        warn!(
557            "signing bundle at {} into {}",
558            self.bundle.root_dir().display(),
559            dest_dir.display()
560        );
561
562        // Frameworks are a bit special.
563        //
564        // Modern frameworks typically have a `Versions/` directory containing directories
565        // with the actual frameworks. These are the actual directories that are signed - not
566        // the top-most directory. In fact, the top-most `.framework` directory doesn't have any
567        // code signature elements at all and can effectively be ignored as far as signing
568        // is concerned.
569        //
570        // But even if there is a `Versions/` directory with nested bundles to sign, the top-level
571        // directory may have some symlinks. And those need to be preserved. In addition, there
572        // may be symlinks in `Versions/`. `Versions/Current` is common.
573        //
574        // Of course, if there is no `Versions/` directory, the top-level directory could be
575        // a valid framework warranting signing.
576        if self.bundle.package_type() == BundlePackageType::Framework {
577            if self.bundle.root_dir().join("Versions").is_dir() {
578                info!("found a versioned framework; each version will be signed as its own bundle");
579
580                // But we still need to preserve files (hopefully just symlinks) outside the
581                // nested bundles under `Versions/`. Since we don't nest into child bundles
582                // here, it should be safe to handle each encountered file.
583                let mut context = BundleSigningContext {
584                    dest_dir: dest_dir.to_path_buf(),
585                    settings,
586                    previously_installed_paths,
587                    installed_paths: Default::default(),
588                };
589
590                for file in self
591                    .bundle
592                    .files(false)
593                    .map_err(AppleCodesignError::DirectoryBundle)?
594                {
595                    context.install_file(file.absolute_path(), file.relative_path())?;
596                }
597
598                let bundle = DirectoryBundle::new_from_path(dest_dir)
599                    .map_err(AppleCodesignError::DirectoryBundle)?;
600
601                return Ok(BundleSigningInfo {
602                    bundle,
603                    installed_rel_paths: context.installed_paths,
604                });
605            } else {
606                info!("found an unversioned framework; signing like normal");
607            }
608        }
609
610        let dest_dir_root = dest_dir.to_path_buf();
611
612        let dest_dir = if self.bundle.shallow() {
613            dest_dir_root.clone()
614        } else {
615            dest_dir.join("Contents")
616        };
617
618        let mut resources_digests = settings.all_digests(SettingsScope::Main);
619
620        // State in the main executable can influence signing settings of the bundle. So examine
621        // it first.
622
623        let main_exe = self
624            .bundle
625            .files(false)
626            .map_err(AppleCodesignError::DirectoryBundle)?
627            .into_iter()
628            .find(|f| matches!(f.is_main_executable(), Ok(true)));
629
630        if let Some(exe) = &main_exe {
631            let macho_data = std::fs::read(exe.absolute_path())?;
632            let mach = MachFile::parse(&macho_data)?;
633
634            for macho in mach.iter_macho() {
635                let need_sha1_sha256 = if let Some(targeting) = macho.find_targeting()? {
636                    let sha256_version = targeting.platform.sha256_digest_support()?;
637
638                    !sha256_version.matches(&targeting.minimum_os_version)
639                } else {
640                    true
641                };
642
643                if need_sha1_sha256
644                    && resources_digests != vec![DigestType::Sha1, DigestType::Sha256]
645                {
646                    info!(
647                        "activating SHA-1 + SHA-256 signing due to requirements of main executable"
648                    );
649                    resources_digests = vec![DigestType::Sha1, DigestType::Sha256];
650                    break;
651                }
652            }
653        }
654
655        info!("collecting code resources files");
656
657        // The set of rules to use is determined by whether the bundle *can* have a
658        // `Resources/`, not whether it necessarily does. The exact rules for this are not
659        // known. Essentially we want to test for the result of CFBundleCopyResourcesDirectoryURL().
660        // We assume that we can use the resources rules when there is a `Resources` directory
661        // (this seems obvious!) or when the bundle isn't shallow, as a non-shallow bundle should
662        // be an app bundle and app bundles can always have resources (we think).
663        let mut resources_builder =
664            if self.bundle.resolve_path("Resources").is_dir() || !self.bundle.shallow() {
665                CodeResourcesBuilder::default_resources_rules()?
666            } else {
667                CodeResourcesBuilder::default_no_resources_rules()?
668            };
669
670        // Ensure emitted digests match what we're configured to emit.
671        resources_builder.set_digests(resources_digests.into_iter());
672
673        // Exclude code signature files we'll write.
674        resources_builder.add_exclusion_rule(CodeResourcesRule::new("^_CodeSignature/")?.exclude());
675        // Ignore notarization ticket.
676        resources_builder.add_exclusion_rule(CodeResourcesRule::new("^CodeResources$")?.exclude());
677        // Ignore store manifest directory.
678        resources_builder.add_exclusion_rule(CodeResourcesRule::new("^_MASReceipt$")?.exclude());
679
680        // The bundle's main executable file's code directory needs to hold a
681        // digest of the CodeResources file for the bundle. Therefore it needs to
682        // be handled last. We add an exclusion rule to prevent the directory walker
683        // from touching this file.
684        if let Some(main_exe) = &main_exe {
685            // Also seal the resources normalized path, just in case it is different.
686            resources_builder.add_exclusion_rule(
687                CodeResourcesRule::new(format!(
688                    "^{}$",
689                    regex::escape(&normalized_resources_path(main_exe.relative_path()))
690                ))?
691                .exclude(),
692            );
693        }
694
695        let mut context = BundleSigningContext {
696            dest_dir: dest_dir_root.clone(),
697            settings,
698            previously_installed_paths,
699            installed_paths: Default::default(),
700        };
701
702        resources_builder.walk_and_seal_directory(
703            &self.root_bundle_path,
704            self.bundle.root_dir(),
705            &mut context,
706        )?;
707
708        let info_plist_data = std::fs::read(self.bundle.info_plist_path())?;
709
710        // The resources are now sealed. Write out that XML file.
711        let code_resources_path = dest_dir.join("_CodeSignature").join("CodeResources");
712        info!(
713            "writing sealed resources to {}",
714            code_resources_path.display()
715        );
716        std::fs::create_dir_all(code_resources_path.parent().unwrap())?;
717        let mut resources_data = Vec::<u8>::new();
718        resources_builder.write_code_resources(&mut resources_data)?;
719
720        {
721            let mut fh = std::fs::File::create(&code_resources_path)?;
722            fh.write_all(&resources_data)?;
723        }
724
725        // Seal the main executable.
726        if let Some(exe) = main_exe {
727            warn!("signing main executable {}", exe.relative_path().display());
728
729            #[cfg(unix)]
730            {
731                use std::os::unix::fs::PermissionsExt;
732                let mut perms = std::fs::metadata(exe.absolute_path())?.permissions();
733                perms.set_mode(0o755);
734                std::fs::set_permissions(exe.absolute_path(), perms)?;
735            }
736
737            let macho_data = std::fs::read(exe.absolute_path())?;
738            let signer = MachOSigner::new(&macho_data)?;
739
740            let mut settings = settings
741                .as_bundle_main_executable_settings(exe.relative_path().to_string_lossy().as_ref());
742
743            // The identifier for the main executable is defined in the bundle's Info.plist.
744            if let Some(ident) = self
745                .bundle
746                .identifier()
747                .map_err(AppleCodesignError::DirectoryBundle)?
748            {
749                info!("setting main executable binary identifier to {} (derived from CFBundleIdentifier in Info.plist)", ident);
750                settings.set_binary_identifier(SettingsScope::Main, ident);
751            } else {
752                info!("unable to determine binary identifier from bundle's Info.plist (CFBundleIdentifier not set?)");
753            }
754
755            settings.set_code_resources_data(SettingsScope::Main, resources_data);
756            settings.set_info_plist_data(SettingsScope::Main, info_plist_data);
757
758            // Important: manually override all settings before calling this so that
759            // explicitly set settings are always used and we don't get misleading logs.
760            // If we set settings after the fact, we may fail to define settings on a
761            // sub-scope, leading the overwrite to not being used.
762            settings.import_settings_from_macho(&macho_data)?;
763
764            let mut new_data = Vec::<u8>::with_capacity(macho_data.len() + 2_usize.pow(17));
765            signer.write_signed_binary(&settings, &mut new_data)?;
766
767            let dest_path = dest_dir_root.join(exe.relative_path());
768            info!("writing signed main executable to {}", dest_path.display());
769            write_macho_file(exe.absolute_path(), &dest_path, &new_data)?;
770
771            context
772                .installed_paths
773                .insert(exe.relative_path().to_path_buf());
774        } else {
775            warn!("bundle has no main executable to sign specially");
776        }
777
778        let bundle = DirectoryBundle::new_from_path(&dest_dir_root)
779            .map_err(AppleCodesignError::DirectoryBundle)?;
780
781        Ok(BundleSigningInfo {
782            bundle,
783            installed_rel_paths: context.installed_paths,
784        })
785    }
786}