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        let macho_data = std::fs::read(source_path)?;
473        let signer = MachOSigner::new(&macho_data)?;
474
475        let mut settings = self
476            .settings
477            .as_bundle_macho_settings(bundle_rel_path.to_string_lossy().as_ref());
478
479        // When signing a Mach-O in the context of a bundle, always define the
480        // binary identifier from the filename so everything is consistent.
481        // Unless an existing setting overrides it, of course.
482        if settings.binary_identifier(SettingsScope::Main).is_none() {
483            let identifier = path_identifier(bundle_rel_path)?;
484            info!("setting binary identifier based on path: {}", identifier);
485
486            settings.set_binary_identifier(SettingsScope::Main, &identifier);
487        }
488
489        settings.import_settings_from_macho(&macho_data)?;
490
491        let mut new_data = Vec::<u8>::with_capacity(macho_data.len() + 2_usize.pow(17));
492        signer.write_signed_binary(&settings, &mut new_data)?;
493
494        let dest_path = self.dest_dir.join(bundle_rel_path);
495
496        info!("writing Mach-O to {}", dest_path.display());
497        write_macho_file(source_path, &dest_path, &new_data)?;
498
499        let info = SignedMachOInfo::parse_data(&new_data)?;
500
501        self.installed_paths.insert(bundle_rel_path.to_path_buf());
502
503        Ok((dest_path, info))
504    }
505}
506
507/// Holds metadata describing the result of a bundle signing operation.
508pub struct BundleSigningInfo {
509    /// The signed bundle.
510    pub bundle: DirectoryBundle,
511
512    /// Bundle relative paths of files that are installed by this signing operation.
513    pub installed_rel_paths: BTreeSet<PathBuf>,
514}
515
516/// A primitive for signing a single Apple bundle.
517///
518/// Unlike [BundleSigner], this type only signs a single bundle and is ignorant
519/// about nested bundles. You probably want to use [BundleSigner] as the interface
520/// for signing bundles, as failure to account for nested bundles can result in
521/// signature verification errors.
522pub struct SingleBundleSigner {
523    /// Path of the root bundle being signed.
524    root_bundle_path: PathBuf,
525
526    /// The bundle being signed.
527    bundle: DirectoryBundle,
528}
529
530impl SingleBundleSigner {
531    /// Construct a new instance.
532    pub fn new(root_bundle_path: PathBuf, bundle: DirectoryBundle) -> Self {
533        Self {
534            root_bundle_path,
535            bundle,
536        }
537    }
538
539    /// Write a signed bundle to the given directory.
540    pub fn write_signed_bundle(
541        &self,
542        dest_dir: impl AsRef<Path>,
543        settings: &SigningSettings,
544        previously_installed_paths: BTreeSet<PathBuf>,
545    ) -> Result<BundleSigningInfo, AppleCodesignError> {
546        let dest_dir = dest_dir.as_ref();
547
548        warn!(
549            "signing bundle at {} into {}",
550            self.bundle.root_dir().display(),
551            dest_dir.display()
552        );
553
554        // Frameworks are a bit special.
555        //
556        // Modern frameworks typically have a `Versions/` directory containing directories
557        // with the actual frameworks. These are the actual directories that are signed - not
558        // the top-most directory. In fact, the top-most `.framework` directory doesn't have any
559        // code signature elements at all and can effectively be ignored as far as signing
560        // is concerned.
561        //
562        // But even if there is a `Versions/` directory with nested bundles to sign, the top-level
563        // directory may have some symlinks. And those need to be preserved. In addition, there
564        // may be symlinks in `Versions/`. `Versions/Current` is common.
565        //
566        // Of course, if there is no `Versions/` directory, the top-level directory could be
567        // a valid framework warranting signing.
568        if self.bundle.package_type() == BundlePackageType::Framework {
569            if self.bundle.root_dir().join("Versions").is_dir() {
570                info!("found a versioned framework; each version will be signed as its own bundle");
571
572                // But we still need to preserve files (hopefully just symlinks) outside the
573                // nested bundles under `Versions/`. Since we don't nest into child bundles
574                // here, it should be safe to handle each encountered file.
575                let mut context = BundleSigningContext {
576                    dest_dir: dest_dir.to_path_buf(),
577                    settings,
578                    previously_installed_paths,
579                    installed_paths: Default::default(),
580                };
581
582                for file in self
583                    .bundle
584                    .files(false)
585                    .map_err(AppleCodesignError::DirectoryBundle)?
586                {
587                    context.install_file(file.absolute_path(), file.relative_path())?;
588                }
589
590                let bundle = DirectoryBundle::new_from_path(dest_dir)
591                    .map_err(AppleCodesignError::DirectoryBundle)?;
592
593                return Ok(BundleSigningInfo {
594                    bundle,
595                    installed_rel_paths: context.installed_paths,
596                });
597            } else {
598                info!("found an unversioned framework; signing like normal");
599            }
600        }
601
602        let dest_dir_root = dest_dir.to_path_buf();
603
604        let dest_dir = if self.bundle.shallow() {
605            dest_dir_root.clone()
606        } else {
607            dest_dir.join("Contents")
608        };
609
610        self.bundle
611            .identifier()
612            .map_err(AppleCodesignError::DirectoryBundle)?
613            .ok_or_else(|| AppleCodesignError::BundleNoIdentifier(self.bundle.info_plist_path()))?;
614
615        let mut resources_digests = settings.all_digests(SettingsScope::Main);
616
617        // State in the main executable can influence signing settings of the bundle. So examine
618        // it first.
619
620        let main_exe = self
621            .bundle
622            .files(false)
623            .map_err(AppleCodesignError::DirectoryBundle)?
624            .into_iter()
625            .find(|f| matches!(f.is_main_executable(), Ok(true)));
626
627        if let Some(exe) = &main_exe {
628            let macho_data = std::fs::read(exe.absolute_path())?;
629            let mach = MachFile::parse(&macho_data)?;
630
631            for macho in mach.iter_macho() {
632                let need_sha1_sha256 = if let Some(targeting) = macho.find_targeting()? {
633                    let sha256_version = targeting.platform.sha256_digest_support()?;
634
635                    !sha256_version.matches(&targeting.minimum_os_version)
636                } else {
637                    true
638                };
639
640                if need_sha1_sha256
641                    && resources_digests != vec![DigestType::Sha1, DigestType::Sha256]
642                {
643                    info!(
644                        "activating SHA-1 + SHA-256 signing due to requirements of main executable"
645                    );
646                    resources_digests = vec![DigestType::Sha1, DigestType::Sha256];
647                    break;
648                }
649            }
650        }
651
652        info!("collecting code resources files");
653
654        // The set of rules to use is determined by whether the bundle *can* have a
655        // `Resources/`, not whether it necessarily does. The exact rules for this are not
656        // known. Essentially we want to test for the result of CFBundleCopyResourcesDirectoryURL().
657        // We assume that we can use the resources rules when there is a `Resources` directory
658        // (this seems obvious!) or when the bundle isn't shallow, as a non-shallow bundle should
659        // be an app bundle and app bundles can always have resources (we think).
660        let mut resources_builder =
661            if self.bundle.resolve_path("Resources").is_dir() || !self.bundle.shallow() {
662                CodeResourcesBuilder::default_resources_rules()?
663            } else {
664                CodeResourcesBuilder::default_no_resources_rules()?
665            };
666
667        // Ensure emitted digests match what we're configured to emit.
668        resources_builder.set_digests(resources_digests.into_iter());
669
670        // Exclude code signature files we'll write.
671        resources_builder.add_exclusion_rule(CodeResourcesRule::new("^_CodeSignature/")?.exclude());
672        // Ignore notarization ticket.
673        resources_builder.add_exclusion_rule(CodeResourcesRule::new("^CodeResources$")?.exclude());
674        // Ignore store manifest directory.
675        resources_builder.add_exclusion_rule(CodeResourcesRule::new("^_MASReceipt$")?.exclude());
676
677        // The bundle's main executable file's code directory needs to hold a
678        // digest of the CodeResources file for the bundle. Therefore it needs to
679        // be handled last. We add an exclusion rule to prevent the directory walker
680        // from touching this file.
681        if let Some(main_exe) = &main_exe {
682            // Also seal the resources normalized path, just in case it is different.
683            resources_builder.add_exclusion_rule(
684                CodeResourcesRule::new(format!(
685                    "^{}$",
686                    regex::escape(&normalized_resources_path(main_exe.relative_path()))
687                ))?
688                .exclude(),
689            );
690        }
691
692        let mut context = BundleSigningContext {
693            dest_dir: dest_dir_root.clone(),
694            settings,
695            previously_installed_paths,
696            installed_paths: Default::default(),
697        };
698
699        resources_builder.walk_and_seal_directory(
700            &self.root_bundle_path,
701            self.bundle.root_dir(),
702            &mut context,
703        )?;
704
705        let info_plist_data = std::fs::read(self.bundle.info_plist_path())?;
706
707        // The resources are now sealed. Write out that XML file.
708        let code_resources_path = dest_dir.join("_CodeSignature").join("CodeResources");
709        info!(
710            "writing sealed resources to {}",
711            code_resources_path.display()
712        );
713        std::fs::create_dir_all(code_resources_path.parent().unwrap())?;
714        let mut resources_data = Vec::<u8>::new();
715        resources_builder.write_code_resources(&mut resources_data)?;
716
717        {
718            let mut fh = std::fs::File::create(&code_resources_path)?;
719            fh.write_all(&resources_data)?;
720        }
721
722        // Seal the main executable.
723        if let Some(exe) = main_exe {
724            warn!("signing main executable {}", exe.relative_path().display());
725
726            let macho_data = std::fs::read(exe.absolute_path())?;
727            let signer = MachOSigner::new(&macho_data)?;
728
729            let mut settings = settings
730                .as_bundle_main_executable_settings(exe.relative_path().to_string_lossy().as_ref());
731
732            // The identifier for the main executable is defined in the bundle's Info.plist.
733            if let Some(ident) = self
734                .bundle
735                .identifier()
736                .map_err(AppleCodesignError::DirectoryBundle)?
737            {
738                info!("setting main executable binary identifier to {} (derived from CFBundleIdentifier in Info.plist)", ident);
739                settings.set_binary_identifier(SettingsScope::Main, ident);
740            } else {
741                info!("unable to determine binary identifier from bundle's Info.plist (CFBundleIdentifier not set?)");
742            }
743
744            settings.set_code_resources_data(SettingsScope::Main, resources_data);
745            settings.set_info_plist_data(SettingsScope::Main, info_plist_data);
746
747            // Important: manually override all settings before calling this so that
748            // explicitly set settings are always used and we don't get misleading logs.
749            // If we set settings after the fact, we may fail to define settings on a
750            // sub-scope, leading the overwrite to not being used.
751            settings.import_settings_from_macho(&macho_data)?;
752
753            let mut new_data = Vec::<u8>::with_capacity(macho_data.len() + 2_usize.pow(17));
754            signer.write_signed_binary(&settings, &mut new_data)?;
755
756            let dest_path = dest_dir_root.join(exe.relative_path());
757            info!("writing signed main executable to {}", dest_path.display());
758            write_macho_file(exe.absolute_path(), &dest_path, &new_data)?;
759
760            context
761                .installed_paths
762                .insert(exe.relative_path().to_path_buf());
763        } else {
764            warn!("bundle has no main executable to sign specially");
765        }
766
767        let bundle = DirectoryBundle::new_from_path(&dest_dir_root)
768            .map_err(AppleCodesignError::DirectoryBundle)?;
769
770        Ok(BundleSigningInfo {
771            bundle,
772            installed_rel_paths: context.installed_paths,
773        })
774    }
775}