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