1use {
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
31pub 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
61pub struct BundleSigner {
69 bundles: BTreeMap<Option<String>, SingleBundleSigner>,
71}
72
73impl BundleSigner {
74 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 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 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 true
131 }
132 (false, _, false) => {
133 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 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 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 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 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 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
272pub struct SignedMachOInfo {
280 pub code_directory_blob: Vec<u8>,
284
285 pub designated_code_requirement: Option<String>,
290}
291
292impl SignedMachOInfo {
293 pub fn parse_data(data: &[u8]) -> Result<Self, AppleCodesignError> {
295 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 let mut requirement_expr = None;
315
316 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 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 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 digest.truncate(20);
385
386 let digest = hex::encode(digest);
387
388 Ok(format!("2/{digest_type}/{digest}"))
390 }
391}
392
393pub struct BundleSigningContext<'a, 'key> {
395 pub settings: &'a SigningSettings<'key>,
397 pub dest_dir: PathBuf,
399 pub previously_installed_paths: BTreeSet<PathBuf>,
403 pub installed_paths: BTreeSet<PathBuf>,
405}
406
407impl<'a, 'key> BundleSigningContext<'a, 'key> {
408 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 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 isideload_vfs::fs::copy(source_path, &dest_path)?;
452 filetime::set_file_mtime(&dest_path, mtime)?;
453 }
454 }
455
456 self.installed_paths.insert(bundle_rel_path.to_path_buf());
460
461 Ok(dest_path)
462 }
463
464 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 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
518pub struct BundleSigningInfo {
520 pub bundle: DirectoryBundle,
522
523 pub installed_rel_paths: BTreeSet<PathBuf>,
525}
526
527pub struct SingleBundleSigner {
534 root_bundle_path: PathBuf,
536
537 bundle: DirectoryBundle,
539}
540
541impl SingleBundleSigner {
542 pub fn new(root_bundle_path: PathBuf, bundle: DirectoryBundle) -> Self {
544 Self {
545 root_bundle_path,
546 bundle,
547 }
548 }
549
550 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 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 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 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 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 resources_builder.set_digests(resources_digests.into_iter());
677
678 resources_builder.add_exclusion_rule(CodeResourcesRule::new("^_CodeSignature/")?.exclude());
680 resources_builder.add_exclusion_rule(CodeResourcesRule::new("^CodeResources$")?.exclude());
682 resources_builder.add_exclusion_rule(CodeResourcesRule::new("^_MASReceipt$")?.exclude());
684
685 if let Some(main_exe) = &main_exe {
690 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 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 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 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 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}