1use {
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
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!("{} 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 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 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
269pub struct SignedMachOInfo {
277 pub code_directory_blob: Vec<u8>,
281
282 pub designated_code_requirement: Option<String>,
287}
288
289impl SignedMachOInfo {
290 pub fn parse_data(data: &[u8]) -> Result<Self, AppleCodesignError> {
292 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 let mut requirement_expr = None;
312
313 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 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 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 digest.truncate(20);
382
383 let digest = hex::encode(digest);
384
385 Ok(format!("2/{digest_type}/{digest}"))
387 }
388}
389
390pub struct BundleSigningContext<'a, 'key> {
392 pub settings: &'a SigningSettings<'key>,
394 pub dest_dir: PathBuf,
396 pub previously_installed_paths: BTreeSet<PathBuf>,
400 pub installed_paths: BTreeSet<PathBuf>,
402}
403
404impl<'a, 'key> BundleSigningContext<'a, 'key> {
405 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 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 std::fs::copy(source_path, &dest_path)?;
449 filetime::set_file_mtime(&dest_path, mtime)?;
450 }
451 }
452
453 self.installed_paths.insert(bundle_rel_path.to_path_buf());
457
458 Ok(dest_path)
459 }
460
461 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 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
507pub struct BundleSigningInfo {
509 pub bundle: DirectoryBundle,
511
512 pub installed_rel_paths: BTreeSet<PathBuf>,
514}
515
516pub struct SingleBundleSigner {
523 root_bundle_path: PathBuf,
525
526 bundle: DirectoryBundle,
528}
529
530impl SingleBundleSigner {
531 pub fn new(root_bundle_path: PathBuf, bundle: DirectoryBundle) -> Self {
533 Self {
534 root_bundle_path,
535 bundle,
536 }
537 }
538
539 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 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 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 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 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 resources_builder.set_digests(resources_digests.into_iter());
669
670 resources_builder.add_exclusion_rule(CodeResourcesRule::new("^_CodeSignature/")?.exclude());
672 resources_builder.add_exclusion_rule(CodeResourcesRule::new("^CodeResources$")?.exclude());
674 resources_builder.add_exclusion_rule(CodeResourcesRule::new("^_MASReceipt$")?.exclude());
676
677 if let Some(main_exe) = &main_exe {
682 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 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 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 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 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}