1use {
10 crate::{
11 code_directory::{CodeDirectoryBlob, CodeSignatureFlags, ExecutableSegmentFlags},
12 code_requirement::{CodeRequirementExpression, CodeRequirements, RequirementType},
13 cryptography::Digest,
14 embedded_signature::{
15 Blob, BlobData, CodeSigningSlot, ConstraintsDerBlob, EntitlementsBlob,
16 EntitlementsDerBlob, RequirementSetBlob,
17 },
18 embedded_signature_builder::EmbeddedSignatureBuilder,
19 entitlements::plist_to_executable_segment_flags,
20 error::AppleCodesignError,
21 macho::{semver_to_macho_target_version, MachFile, MachOBinary},
22 macho_universal::create_universal_macho,
23 policy::derive_designated_requirements,
24 signing_settings::{DesignatedRequirementMode, SettingsScope, SigningSettings},
25 },
26 goblin::mach::{
27 constants::{SEG_LINKEDIT, SEG_PAGEZERO},
28 load_command::{
29 CommandVariant, LinkeditDataCommand, SegmentCommand32, SegmentCommand64,
30 LC_CODE_SIGNATURE, SIZEOF_LINKEDIT_DATA_COMMAND,
31 },
32 parse_magic_and_ctx,
33 },
34 log::{debug, info, warn},
35 scroll::{ctx::SizeWith, IOwrite},
36 std::{borrow::Cow, cmp::Ordering, collections::HashMap, io::Write, path::Path},
37};
38
39fn create_macho_with_signature(
41 macho: &MachOBinary,
42 signature_data: &[u8],
43) -> Result<Vec<u8>, AppleCodesignError> {
44 macho.check_signing_capability()?;
46
47 let linkedit_data_before_signature = macho
59 .linkedit_data_before_signature()
60 .ok_or(AppleCodesignError::MissingLinkedit)?;
61
62 let signature_file_offset = macho.code_limit_binary_offset()?;
63 let remainder = (signature_file_offset % 16) as usize;
64 let signature_padding_length = if remainder == 0 { 0 } else { 16 - remainder };
65
66 let signature_file_offset = signature_file_offset + signature_padding_length as u64;
67
68 let new_linkedit_segment_size =
69 linkedit_data_before_signature.len() + signature_padding_length + signature_data.len();
70
71 let remainder = new_linkedit_segment_size % 16384;
74 let new_linkedit_segment_vmsize = if remainder == 0 {
75 new_linkedit_segment_size
76 } else {
77 new_linkedit_segment_size + 16384 - remainder
78 };
79
80 assert!(new_linkedit_segment_vmsize >= new_linkedit_segment_size);
81 assert_eq!(new_linkedit_segment_vmsize % 16384, 0);
82
83 let mut cursor = std::io::Cursor::new(Vec::<u8>::new());
84
85 let ctx = parse_magic_and_ctx(macho.data, 0)?
88 .1
89 .expect("context should have been parsed before");
90
91 let mut header = macho.macho.header;
94 if macho.code_signature_load_command().is_none() {
95 header.ncmds += 1;
96 header.sizeofcmds += SIZEOF_LINKEDIT_DATA_COMMAND as u32;
97 }
98
99 cursor.iowrite_with(header, ctx)?;
100
101 let mut seen_signature_load_command = false;
105
106 for load_command in &macho.macho.load_commands {
107 let original_command_data =
108 &macho.data[load_command.offset..load_command.offset + load_command.command.cmdsize()];
109
110 let written_len = match &load_command.command {
111 CommandVariant::CodeSignature(command) => {
112 seen_signature_load_command = true;
113
114 let mut command = *command;
115 command.dataoff = signature_file_offset as _;
116 command.datasize = signature_data.len() as _;
117
118 cursor.iowrite_with(command, ctx.le)?;
119
120 LinkeditDataCommand::size_with(&ctx.le)
121 }
122 CommandVariant::Segment32(segment) => {
123 let segment = match segment.name() {
124 Ok(SEG_LINKEDIT) => {
125 let mut segment = *segment;
126 segment.filesize = new_linkedit_segment_size as _;
127 segment.vmsize = new_linkedit_segment_vmsize as _;
128
129 segment
130 }
131 _ => *segment,
132 };
133
134 cursor.iowrite_with(segment, ctx.le)?;
135
136 SegmentCommand32::size_with(&ctx.le)
137 }
138 CommandVariant::Segment64(segment) => {
139 let segment = match segment.name() {
140 Ok(SEG_LINKEDIT) => {
141 let mut segment = *segment;
142 segment.filesize = new_linkedit_segment_size as _;
143 segment.vmsize = new_linkedit_segment_vmsize as _;
144
145 segment
146 }
147 _ => *segment,
148 };
149
150 cursor.iowrite_with(segment, ctx.le)?;
151
152 SegmentCommand64::size_with(&ctx.le)
153 }
154 _ => {
155 cursor.write_all(original_command_data)?;
157 original_command_data.len()
158 }
159 };
160
161 cursor.write_all(&original_command_data[written_len..])?;
164 }
165
166 if !seen_signature_load_command {
174 let command = LinkeditDataCommand {
175 cmd: LC_CODE_SIGNATURE,
176 cmdsize: SIZEOF_LINKEDIT_DATA_COMMAND as _,
177 dataoff: signature_file_offset as _,
178 datasize: signature_data.len() as _,
179 };
180
181 cursor.iowrite_with(command, ctx.le)?;
182 }
183
184 let mut wrote_non_empty_segment = false;
185
186 for segment in macho.segments_by_file_offset() {
188 if matches!(segment.name(), Ok(SEG_PAGEZERO)) {
191 continue;
192 }
193
194 match cursor.position().cmp(&segment.fileoff) {
195 Ordering::Less => {
198 let padding = &macho.data[cursor.position() as usize..segment.fileoff as usize];
199 debug!(
200 "copying {} bytes outside segment boundaries before segment {}",
201 padding.len(),
202 segment.name().unwrap_or("<unknown>")
203 );
204 cursor.write_all(padding)?;
205 }
206
207 Ordering::Greater if segment.fileoff == 0 => {}
210
211 Ordering::Greater if !wrote_non_empty_segment => {}
219
220 Ordering::Greater => {
223 return Err(AppleCodesignError::MachOWrite(format!(
224 "Mach-O segment corruption: cursor at 0x{:x} but segment begins at 0x{:x} (please report this bug)",
225 cursor.position(),
226 segment.fileoff
227 )));
228 }
229 Ordering::Equal => {}
230 }
231
232 match segment.name() {
233 Ok(SEG_LINKEDIT) => {
234 cursor.write_all(
235 macho
236 .linkedit_data_before_signature()
237 .expect("__LINKEDIT segment data should resolve"),
238 )?;
239
240 let padding = vec![0u8; signature_padding_length];
241 cursor.write_all(&padding)?;
242
243 assert_eq!(cursor.position(), signature_file_offset);
244 assert_eq!(cursor.position() % 16, 0);
245 cursor.write_all(signature_data)?;
246 }
247 _ => {
248 if segment.fileoff < cursor.position() {
252 if segment.data.is_empty() {
253 continue;
254 }
255 let remaining =
256 &segment.data[cursor.position() as usize..segment.filesize as usize];
257 cursor.write_all(remaining)?;
258 } else {
259 cursor.write_all(segment.data)?;
260 }
261 }
262 }
263
264 wrote_non_empty_segment = true;
265 }
266
267 Ok(cursor.into_inner())
268}
269
270pub fn write_macho_file(
272 input_path: &Path,
273 output_path: &Path,
274 macho_data: &[u8],
275) -> Result<(), AppleCodesignError> {
276 let permissions = std::fs::metadata(input_path)?.permissions();
278
279 if let Some(parent) = output_path.parent() {
280 std::fs::create_dir_all(parent)?;
281 }
282
283 {
284 let mut fh = std::fs::File::create(output_path)?;
285 fh.write_all(macho_data)?;
286 }
287
288 std::fs::set_permissions(output_path, permissions)?;
289
290 Ok(())
291}
292
293pub struct MachOSigner<'data> {
314 machos: Vec<MachOBinary<'data>>,
316}
317
318impl<'data> MachOSigner<'data> {
319 pub fn new(macho_data: &'data [u8]) -> Result<Self, AppleCodesignError> {
324 let machos = MachFile::parse(macho_data)?.into_iter().collect::<Vec<_>>();
325
326 Ok(Self { machos })
327 }
328
329 pub fn write_signed_binary(
331 &self,
332 settings: &SigningSettings,
333 writer: &mut impl Write,
334 ) -> Result<(), AppleCodesignError> {
335 let binaries = self
340 .machos
341 .iter()
342 .enumerate()
343 .map(|(index, original_macho)| {
344 info!("signing Mach-O binary at index {}", index);
345 let settings = settings
346 .as_universal_macho_settings(index, original_macho.macho.header.cputype());
347
348 let signature_len =
349 self.estimate_embedded_signature_size(original_macho, &settings)?;
350
351 let placeholder_signature_data = b"\0".repeat(signature_len);
354
355 let intermediate_macho_data =
356 create_macho_with_signature(original_macho, &placeholder_signature_data)?;
357
358 let intermediate_macho = MachOBinary::parse(&intermediate_macho_data)?;
360
361 let mut signature_data = self.create_superblob(&settings, &intermediate_macho)?;
362 info!("total signature size: {} bytes", signature_data.len());
363
364 match signature_data.len().cmp(&placeholder_signature_data.len()) {
367 Ordering::Greater => {
368 return Err(AppleCodesignError::SignatureDataTooLarge);
369 }
370 Ordering::Equal => {}
371 Ordering::Less => {
372 signature_data.extend_from_slice(
373 &b"\0".repeat(placeholder_signature_data.len() - signature_data.len()),
374 );
375 }
376 }
377
378 create_macho_with_signature(&intermediate_macho, &signature_data)
379 })
380 .collect::<Result<Vec<_>, AppleCodesignError>>()?;
381
382 if binaries.len() > 1 {
383 create_universal_macho(writer, binaries.iter().map(|x| x.as_slice()))?;
384 } else {
385 writer.write_all(&binaries[0])?;
386 }
387
388 Ok(())
389 }
390
391 pub fn create_superblob(
400 &self,
401 settings: &SigningSettings,
402 macho: &MachOBinary,
403 ) -> Result<Vec<u8>, AppleCodesignError> {
404 let mut builder = EmbeddedSignatureBuilder::default();
405
406 for (slot, blob) in self.create_special_blobs(settings, macho.is_executable())? {
407 builder.add_blob(slot, blob)?;
408 }
409
410 let code_directory = self.create_code_directory(settings, macho)?;
411 info!("code directory version: {}", code_directory.version);
412
413 builder.add_code_directory(CodeSigningSlot::CodeDirectory, code_directory)?;
414
415 if let Some(digests) = settings.extra_digests(SettingsScope::Main) {
416 for digest_type in digests {
417 let mut alt_settings = settings.clone();
420 alt_settings.set_digest_type(SettingsScope::Main, *digest_type);
421
422 info!(
423 "adding alternative code directory using digest {:?}",
424 digest_type
425 );
426 let cd = self.create_code_directory(&alt_settings, macho)?;
427
428 builder.add_alternative_code_directory(cd)?;
429 }
430 }
431
432 if let Some((signing_key, signing_cert)) = settings.signing_key() {
433 builder.create_cms_signature(
434 signing_key,
435 signing_cert,
436 settings.time_stamp_url(),
437 settings.certificate_chain().iter().cloned(),
438 settings.signing_time(),
439 )?;
440 } else {
441 builder.create_empty_cms_signature()?;
442 }
443
444 builder.create_superblob()
445 }
446
447 pub fn create_code_directory(
453 &self,
454 settings: &SigningSettings,
455 macho: &MachOBinary,
456 ) -> Result<CodeDirectoryBlob<'static>, AppleCodesignError> {
457 let target = macho.find_targeting()?;
461
462 if let Some(target) = &target {
463 info!(
464 "binary targets {} >= {} with SDK {}",
465 target.platform, target.minimum_os_version, target.sdk_version,
466 );
467 }
468
469 let mut flags = CodeSignatureFlags::empty();
470
471 if let Some(additional) = settings.code_signature_flags(SettingsScope::Main) {
472 info!(
473 "adding code signature flags from signing settings: {:?}",
474 additional
475 );
476 flags |= additional;
477 }
478
479 if settings.signing_key().is_none() {
481 info!("creating ad-hoc signature");
482 flags |= CodeSignatureFlags::ADHOC;
483 } else if flags.contains(CodeSignatureFlags::ADHOC) {
484 info!("removing ad-hoc code signature flag");
485 flags -= CodeSignatureFlags::ADHOC;
486 }
487
488 if flags.contains(CodeSignatureFlags::LINKER_SIGNED) {
490 info!("removing linker signed flag from code signature (we're not a linker)");
491 flags -= CodeSignatureFlags::LINKER_SIGNED;
492 }
493
494 let (code_limit, code_limit_64) = match macho.code_limit_binary_offset()? {
498 x if x > u32::MAX as u64 => (0, Some(x)),
499 x => (x as u32, None),
500 };
501
502 let platform = 0;
503 let page_size = 4096u32;
504
505 let (exec_seg_base, exec_seg_limit) = macho.executable_segment_boundary()?;
506 let (exec_seg_base, exec_seg_limit) = (Some(exec_seg_base), Some(exec_seg_limit));
507
508 let exec_seg_flags = if macho.is_executable() {
516 if let Some(entitlements) = settings.entitlements_plist(SettingsScope::Main) {
517 let flags = plist_to_executable_segment_flags(entitlements);
518
519 if !flags.is_empty() {
520 info!("entitlements imply executable segment flags: {:?}", flags);
521 }
522
523 Some(flags | ExecutableSegmentFlags::MAIN_BINARY)
524 } else {
525 Some(ExecutableSegmentFlags::MAIN_BINARY)
526 }
527 } else {
528 None
529 };
530
531 let runtime = match settings.runtime_version(SettingsScope::Main) {
539 Some(version) => {
540 info!(
541 "using hardened runtime version {} from signing settings",
542 version
543 );
544 Some(semver_to_macho_target_version(version))
545 }
546 None => None,
547 };
548
549 let runtime = if runtime.is_none() && flags.contains(CodeSignatureFlags::RUNTIME) {
551 if let Some(target) = &target {
552 info!(
553 "using hardened runtime version {} derived from SDK version",
554 target.sdk_version
555 );
556 Some(semver_to_macho_target_version(&target.sdk_version))
557 } else {
558 warn!("hardened runtime version required but unable to derive suitable version; signature will likely fail Apple checks");
559 None
560 }
561 } else {
562 runtime
563 };
564
565 let digest_type = settings.digest_type(SettingsScope::Main);
566
567 let code_hashes = macho
568 .code_digests(digest_type, page_size as _)?
569 .into_iter()
570 .map(|v| Digest { data: v.into() })
571 .collect::<Vec<_>>();
572
573 let mut special_hashes = HashMap::new();
574
575 if let Some(data) = settings.info_plist_data(SettingsScope::Main) {
578 special_hashes.insert(
579 CodeSigningSlot::Info,
580 Digest {
581 data: digest_type.digest_data(data)?.into(),
582 },
583 );
584 }
585
586 if let Some(data) = settings.code_resources_data(SettingsScope::Main) {
589 special_hashes.insert(
590 CodeSigningSlot::ResourceDir,
591 Digest {
592 data: digest_type.digest_data(data)?.into(),
593 }
594 .to_owned(),
595 );
596 }
597
598 let ident = Cow::Owned(
599 settings
600 .binary_identifier(SettingsScope::Main)
601 .ok_or(AppleCodesignError::NoIdentifier)?
602 .to_string(),
603 );
604
605 let team_name = settings.team_id().map(|x| Cow::Owned(x.to_string()));
609
610 if team_name.is_some() && !settings.signing_certificate_apple_signed() {
611 warn!("signing without an Apple signed certificate but signing settings contain a team name; signature varies from Apple's tooling");
612 }
613
614 let mut cd = CodeDirectoryBlob {
615 flags,
616 code_limit,
617 digest_size: digest_type.hash_len()? as u8,
618 digest_type,
619 platform,
620 page_size,
621 code_limit_64,
622 exec_seg_base,
623 exec_seg_limit,
624 exec_seg_flags,
625 runtime,
626 ident,
627 team_name,
628 code_digests: code_hashes,
629 ..Default::default()
630 };
631
632 for (slot, digest) in special_hashes {
633 cd.set_slot_digest(slot, digest)?;
634 }
635
636 cd.adjust_version(target);
637 cd.clear_newer_fields();
638
639 Ok(cd)
640 }
641
642 pub fn create_special_blobs(
650 &self,
651 settings: &SigningSettings,
652 is_executable: bool,
653 ) -> Result<Vec<(CodeSigningSlot, BlobData<'static>)>, AppleCodesignError> {
654 let mut res = Vec::new();
655
656 let mut requirements = CodeRequirements::default();
657
658 match settings.designated_requirement(SettingsScope::Main) {
659 DesignatedRequirementMode::Auto => {
660 if let Some((_, cert)) = settings.signing_key() {
663 info!("deriving code requirements from signing certificate");
664 let identifier = Some(
665 settings
666 .binary_identifier(SettingsScope::Main)
667 .ok_or(AppleCodesignError::NoIdentifier)?
668 .to_string(),
669 );
670
671 let expr = derive_designated_requirements(
672 cert,
673 settings.certificate_chain(),
674 identifier,
675 )?;
676 requirements.push(expr);
677 }
678 }
679 DesignatedRequirementMode::Explicit(exprs) => {
680 info!("using provided code requirements");
681 for expr in exprs {
682 requirements.push(CodeRequirementExpression::from_bytes(expr)?.0);
683 }
684 }
685 }
686
687 let mut blob = RequirementSetBlob::default();
690
691 if !requirements.is_empty() {
692 requirements.add_to_requirement_set(&mut blob, RequirementType::Designated)?;
693 }
694
695 res.push((CodeSigningSlot::RequirementSet, blob.into()));
696
697 if let Some(entitlements) = settings.entitlements_xml(SettingsScope::Main)? {
698 let blob = EntitlementsBlob::from_string(&entitlements);
699
700 res.push((CodeSigningSlot::Entitlements, blob.into()));
701 }
702
703 if is_executable {
709 if let Some(value) = settings.entitlements_plist(SettingsScope::Main) {
710 let blob = EntitlementsDerBlob::from_plist(value)?;
711
712 res.push((CodeSigningSlot::EntitlementsDer, blob.into()));
713 }
714 }
715
716 if let Some(constraints) = settings.launch_constraints_self(SettingsScope::Main) {
717 let blob = ConstraintsDerBlob::from_encoded_constraints(constraints)?;
718 res.push((CodeSigningSlot::LaunchConstraintsSelf, blob.into()));
719 }
720
721 if let Some(constraints) = settings.launch_constraints_parent(SettingsScope::Main) {
722 let blob = ConstraintsDerBlob::from_encoded_constraints(constraints)?;
723 res.push((CodeSigningSlot::LaunchConstraintsParent, blob.into()));
724 }
725
726 if let Some(constraints) = settings.launch_constraints_responsible(SettingsScope::Main) {
727 let blob = ConstraintsDerBlob::from_encoded_constraints(constraints)?;
728 res.push((
729 CodeSigningSlot::LaunchConstraintsResponsibleProcess,
730 blob.into(),
731 ));
732 }
733
734 if let Some(constraints) = settings.library_constraints(SettingsScope::Main) {
735 let blob = ConstraintsDerBlob::from_encoded_constraints(constraints)?;
736 res.push((CodeSigningSlot::LibraryConstraints, blob.into()));
737 }
738
739 Ok(res)
740 }
741
742 pub fn estimate_embedded_signature_size(
744 &self,
745 macho: &MachOBinary,
746 settings: &SigningSettings,
747 ) -> Result<usize, AppleCodesignError> {
748 let code_directory_count = 1 + settings
749 .extra_digests(SettingsScope::Main)
750 .map(|x| x.len())
751 .unwrap_or_default();
752
753 let mut size = 1024 * code_directory_count;
755
756 size += macho.code_digests_size(settings.digest_type(SettingsScope::Main), 4096)?;
758
759 if let Some(digests) = settings.extra_digests(SettingsScope::Main) {
760 for digest in digests {
761 size += macho.code_digests_size(*digest, 4096)?;
762 }
763 }
764
765 for (_, blob) in self.create_special_blobs(settings, true)? {
767 size += blob.to_blob_bytes()?.len();
768 }
769
770 size += 4096;
772
773 for cert in settings.certificate_chain() {
775 size += cert.constructed_data().len();
776 }
777
778 if settings.time_stamp_url().is_some() {
787 size += 8192;
788 }
789
790 size += 1024 - size % 1024;
792
793 Ok(size)
794 }
795}