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 #[cfg(unix)]
473 {
474 use std::os::unix::fs::PermissionsExt;
475 let mut perms = std::fs::metadata(source_path)?.permissions();
476 perms.set_mode(0o755);
477 std::fs::set_permissions(source_path, perms)?;
478 }
479
480 let macho_data = std::fs::read(source_path)?;
481 let signer = MachOSigner::new(&macho_data)?;
482
483 let mut settings = self
484 .settings
485 .as_bundle_macho_settings(bundle_rel_path.to_string_lossy().as_ref());
486
487 if settings.binary_identifier(SettingsScope::Main).is_none() {
491 let identifier = path_identifier(bundle_rel_path)?;
492 info!("setting binary identifier based on path: {}", identifier);
493
494 settings.set_binary_identifier(SettingsScope::Main, &identifier);
495 }
496
497 settings.import_settings_from_macho(&macho_data)?;
498
499 let mut new_data = Vec::<u8>::with_capacity(macho_data.len() + 2_usize.pow(17));
500 signer.write_signed_binary(&settings, &mut new_data)?;
501
502 let dest_path = self.dest_dir.join(bundle_rel_path);
503
504 info!("writing Mach-O to {}", dest_path.display());
505 write_macho_file(source_path, &dest_path, &new_data)?;
506
507 let info = SignedMachOInfo::parse_data(&new_data)?;
508
509 self.installed_paths.insert(bundle_rel_path.to_path_buf());
510
511 Ok((dest_path, info))
512 }
513}
514
515pub struct BundleSigningInfo {
517 pub bundle: DirectoryBundle,
519
520 pub installed_rel_paths: BTreeSet<PathBuf>,
522}
523
524pub struct SingleBundleSigner {
531 root_bundle_path: PathBuf,
533
534 bundle: DirectoryBundle,
536}
537
538impl SingleBundleSigner {
539 pub fn new(root_bundle_path: PathBuf, bundle: DirectoryBundle) -> Self {
541 Self {
542 root_bundle_path,
543 bundle,
544 }
545 }
546
547 pub fn write_signed_bundle(
549 &self,
550 dest_dir: impl AsRef<Path>,
551 settings: &SigningSettings,
552 previously_installed_paths: BTreeSet<PathBuf>,
553 ) -> Result<BundleSigningInfo, AppleCodesignError> {
554 let dest_dir = dest_dir.as_ref();
555
556 warn!(
557 "signing bundle at {} into {}",
558 self.bundle.root_dir().display(),
559 dest_dir.display()
560 );
561
562 if self.bundle.package_type() == BundlePackageType::Framework {
577 if self.bundle.root_dir().join("Versions").is_dir() {
578 info!("found a versioned framework; each version will be signed as its own bundle");
579
580 let mut context = BundleSigningContext {
584 dest_dir: dest_dir.to_path_buf(),
585 settings,
586 previously_installed_paths,
587 installed_paths: Default::default(),
588 };
589
590 for file in self
591 .bundle
592 .files(false)
593 .map_err(AppleCodesignError::DirectoryBundle)?
594 {
595 context.install_file(file.absolute_path(), file.relative_path())?;
596 }
597
598 let bundle = DirectoryBundle::new_from_path(dest_dir)
599 .map_err(AppleCodesignError::DirectoryBundle)?;
600
601 return Ok(BundleSigningInfo {
602 bundle,
603 installed_rel_paths: context.installed_paths,
604 });
605 } else {
606 info!("found an unversioned framework; signing like normal");
607 }
608 }
609
610 let dest_dir_root = dest_dir.to_path_buf();
611
612 let dest_dir = if self.bundle.shallow() {
613 dest_dir_root.clone()
614 } else {
615 dest_dir.join("Contents")
616 };
617
618 let mut resources_digests = settings.all_digests(SettingsScope::Main);
619
620 let main_exe = self
624 .bundle
625 .files(false)
626 .map_err(AppleCodesignError::DirectoryBundle)?
627 .into_iter()
628 .find(|f| matches!(f.is_main_executable(), Ok(true)));
629
630 if let Some(exe) = &main_exe {
631 let macho_data = std::fs::read(exe.absolute_path())?;
632 let mach = MachFile::parse(&macho_data)?;
633
634 for macho in mach.iter_macho() {
635 let need_sha1_sha256 = if let Some(targeting) = macho.find_targeting()? {
636 let sha256_version = targeting.platform.sha256_digest_support()?;
637
638 !sha256_version.matches(&targeting.minimum_os_version)
639 } else {
640 true
641 };
642
643 if need_sha1_sha256
644 && resources_digests != vec![DigestType::Sha1, DigestType::Sha256]
645 {
646 info!(
647 "activating SHA-1 + SHA-256 signing due to requirements of main executable"
648 );
649 resources_digests = vec![DigestType::Sha1, DigestType::Sha256];
650 break;
651 }
652 }
653 }
654
655 info!("collecting code resources files");
656
657 let mut resources_builder =
664 if self.bundle.resolve_path("Resources").is_dir() || !self.bundle.shallow() {
665 CodeResourcesBuilder::default_resources_rules()?
666 } else {
667 CodeResourcesBuilder::default_no_resources_rules()?
668 };
669
670 resources_builder.set_digests(resources_digests.into_iter());
672
673 resources_builder.add_exclusion_rule(CodeResourcesRule::new("^_CodeSignature/")?.exclude());
675 resources_builder.add_exclusion_rule(CodeResourcesRule::new("^CodeResources$")?.exclude());
677 resources_builder.add_exclusion_rule(CodeResourcesRule::new("^_MASReceipt$")?.exclude());
679
680 if let Some(main_exe) = &main_exe {
685 resources_builder.add_exclusion_rule(
687 CodeResourcesRule::new(format!(
688 "^{}$",
689 regex::escape(&normalized_resources_path(main_exe.relative_path()))
690 ))?
691 .exclude(),
692 );
693 }
694
695 let mut context = BundleSigningContext {
696 dest_dir: dest_dir_root.clone(),
697 settings,
698 previously_installed_paths,
699 installed_paths: Default::default(),
700 };
701
702 resources_builder.walk_and_seal_directory(
703 &self.root_bundle_path,
704 self.bundle.root_dir(),
705 &mut context,
706 )?;
707
708 let info_plist_data = std::fs::read(self.bundle.info_plist_path())?;
709
710 let code_resources_path = dest_dir.join("_CodeSignature").join("CodeResources");
712 info!(
713 "writing sealed resources to {}",
714 code_resources_path.display()
715 );
716 std::fs::create_dir_all(code_resources_path.parent().unwrap())?;
717 let mut resources_data = Vec::<u8>::new();
718 resources_builder.write_code_resources(&mut resources_data)?;
719
720 {
721 let mut fh = std::fs::File::create(&code_resources_path)?;
722 fh.write_all(&resources_data)?;
723 }
724
725 if let Some(exe) = main_exe {
727 warn!("signing main executable {}", exe.relative_path().display());
728
729 #[cfg(unix)]
730 {
731 use std::os::unix::fs::PermissionsExt;
732 let mut perms = std::fs::metadata(exe.absolute_path())?.permissions();
733 perms.set_mode(0o755);
734 std::fs::set_permissions(exe.absolute_path(), perms)?;
735 }
736
737 let macho_data = std::fs::read(exe.absolute_path())?;
738 let signer = MachOSigner::new(&macho_data)?;
739
740 let mut settings = settings
741 .as_bundle_main_executable_settings(exe.relative_path().to_string_lossy().as_ref());
742
743 if let Some(ident) = self
745 .bundle
746 .identifier()
747 .map_err(AppleCodesignError::DirectoryBundle)?
748 {
749 info!("setting main executable binary identifier to {} (derived from CFBundleIdentifier in Info.plist)", ident);
750 settings.set_binary_identifier(SettingsScope::Main, ident);
751 } else {
752 info!("unable to determine binary identifier from bundle's Info.plist (CFBundleIdentifier not set?)");
753 }
754
755 settings.set_code_resources_data(SettingsScope::Main, resources_data);
756 settings.set_info_plist_data(SettingsScope::Main, info_plist_data);
757
758 settings.import_settings_from_macho(&macho_data)?;
763
764 let mut new_data = Vec::<u8>::with_capacity(macho_data.len() + 2_usize.pow(17));
765 signer.write_signed_binary(&settings, &mut new_data)?;
766
767 let dest_path = dest_dir_root.join(exe.relative_path());
768 info!("writing signed main executable to {}", dest_path.display());
769 write_macho_file(exe.absolute_path(), &dest_path, &new_data)?;
770
771 context
772 .installed_paths
773 .insert(exe.relative_path().to_path_buf());
774 } else {
775 warn!("bundle has no main executable to sign specially");
776 }
777
778 let bundle = DirectoryBundle::new_from_path(&dest_dir_root)
779 .map_err(AppleCodesignError::DirectoryBundle)?;
780
781 Ok(BundleSigningInfo {
782 bundle,
783 installed_rel_paths: context.installed_paths,
784 })
785 }
786}