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::{MachFile, MachOBinary, semver_to_macho_target_version},
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, LC_CODE_SIGNATURE, LinkeditDataCommand, SIZEOF_LINKEDIT_DATA_COMMAND,
30 SegmentCommand32, SegmentCommand64,
31 },
32 parse_magic_and_ctx,
33 },
34 log::{debug, info, warn},
35 scroll::{IOwrite, ctx::SizeWith},
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 for binary in binaries {
386 writer.write(&binary)?;
387 }
388 }
389
390 Ok(())
391 }
392
393 pub fn create_superblob(
402 &self,
403 settings: &SigningSettings,
404 macho: &MachOBinary,
405 ) -> Result<Vec<u8>, AppleCodesignError> {
406 let mut builder = EmbeddedSignatureBuilder::default();
407
408 for (slot, blob) in self.create_special_blobs(settings, macho.is_executable())? {
409 builder.add_blob(slot, blob)?;
410 }
411
412 let code_directory = self.create_code_directory(settings, macho)?;
413 info!("code directory version: {}", code_directory.version);
414
415 builder.add_code_directory(CodeSigningSlot::CodeDirectory, code_directory)?;
416
417 if let Some(digests) = settings.extra_digests(SettingsScope::Main) {
418 for digest_type in digests {
419 let mut alt_settings = settings.clone();
422 alt_settings.set_digest_type(SettingsScope::Main, *digest_type);
423
424 info!(
425 "adding alternative code directory using digest {:?}",
426 digest_type
427 );
428 let cd = self.create_code_directory(&alt_settings, macho)?;
429
430 builder.add_alternative_code_directory(cd)?;
431 }
432 }
433
434 if let Some((signing_key, signing_cert)) = settings.signing_key() {
435 builder.create_cms_signature(
436 signing_key,
437 signing_cert,
438 settings.certificate_chain().iter().cloned(),
439 settings.signing_time(),
440 )?;
441 } else {
442 builder.create_empty_cms_signature()?;
443 }
444
445 builder.create_superblob()
446 }
447
448 pub fn create_code_directory(
454 &self,
455 settings: &SigningSettings,
456 macho: &MachOBinary,
457 ) -> Result<CodeDirectoryBlob<'static>, AppleCodesignError> {
458 let target = macho.find_targeting()?;
462
463 if let Some(target) = &target {
464 info!(
465 "binary targets {} >= {} with SDK {}",
466 target.platform, target.minimum_os_version, target.sdk_version,
467 );
468 }
469
470 let mut flags = CodeSignatureFlags::empty();
471
472 if let Some(additional) = settings.code_signature_flags(SettingsScope::Main) {
473 info!(
474 "adding code signature flags from signing settings: {:?}",
475 additional
476 );
477 flags |= additional;
478 }
479
480 if settings.signing_key().is_none() {
482 info!("creating ad-hoc signature");
483 flags |= CodeSignatureFlags::ADHOC;
484 } else if flags.contains(CodeSignatureFlags::ADHOC) {
485 info!("removing ad-hoc code signature flag");
486 flags -= CodeSignatureFlags::ADHOC;
487 }
488
489 if flags.contains(CodeSignatureFlags::LINKER_SIGNED) {
491 info!("removing linker signed flag from code signature (we're not a linker)");
492 flags -= CodeSignatureFlags::LINKER_SIGNED;
493 }
494
495 let (code_limit, code_limit_64) = match macho.code_limit_binary_offset()? {
499 x if x > u32::MAX as u64 => (0, Some(x)),
500 x => (x as u32, None),
501 };
502
503 let platform = 0;
504 let page_size = 4096u32;
505
506 let (exec_seg_base, exec_seg_limit) = macho.executable_segment_boundary()?;
507 let (exec_seg_base, exec_seg_limit) = (Some(exec_seg_base), Some(exec_seg_limit));
508
509 let exec_seg_flags = if macho.is_executable() {
517 if let Some(entitlements) = settings.entitlements_plist(SettingsScope::Main) {
518 let flags = plist_to_executable_segment_flags(entitlements);
519
520 if !flags.is_empty() {
521 info!("entitlements imply executable segment flags: {:?}", flags);
522 }
523
524 Some(flags | ExecutableSegmentFlags::MAIN_BINARY)
525 } else {
526 Some(ExecutableSegmentFlags::MAIN_BINARY)
527 }
528 } else {
529 None
530 };
531
532 let runtime = match settings.runtime_version(SettingsScope::Main) {
540 Some(version) => {
541 info!(
542 "using hardened runtime version {} from signing settings",
543 version
544 );
545 Some(semver_to_macho_target_version(version))
546 }
547 None => None,
548 };
549
550 let runtime = if runtime.is_none() && flags.contains(CodeSignatureFlags::RUNTIME) {
552 if let Some(target) = &target {
553 info!(
554 "using hardened runtime version {} derived from SDK version",
555 target.sdk_version
556 );
557 Some(semver_to_macho_target_version(&target.sdk_version))
558 } else {
559 warn!(
560 "hardened runtime version required but unable to derive suitable version; signature will likely fail Apple checks"
561 );
562 None
563 }
564 } else {
565 runtime
566 };
567
568 let digest_type = settings.digest_type(SettingsScope::Main);
569
570 let code_hashes = macho
571 .code_digests(digest_type, page_size as _)?
572 .into_iter()
573 .map(|v| Digest { data: v.into() })
574 .collect::<Vec<_>>();
575
576 let mut special_hashes = HashMap::new();
577
578 if let Some(data) = settings.info_plist_data(SettingsScope::Main) {
581 special_hashes.insert(
582 CodeSigningSlot::Info,
583 Digest {
584 data: digest_type.digest_data(data)?.into(),
585 },
586 );
587 }
588
589 if let Some(data) = settings.code_resources_data(SettingsScope::Main) {
592 special_hashes.insert(
593 CodeSigningSlot::ResourceDir,
594 Digest {
595 data: digest_type.digest_data(data)?.into(),
596 }
597 .to_owned(),
598 );
599 }
600
601 let ident = Cow::Owned(
602 settings
603 .binary_identifier(SettingsScope::Main)
604 .ok_or(AppleCodesignError::NoIdentifier)?
605 .to_string(),
606 );
607
608 let team_name = settings.team_id().map(|x| Cow::Owned(x.to_string()));
612
613 if team_name.is_some() && !settings.signing_certificate_apple_signed() {
614 warn!(
615 "signing without an Apple signed certificate but signing settings contain a team name; signature varies from Apple's tooling"
616 );
617 }
618
619 let mut cd = CodeDirectoryBlob {
620 flags,
621 code_limit,
622 digest_size: digest_type.hash_len()? as u8,
623 digest_type,
624 platform,
625 page_size,
626 code_limit_64,
627 exec_seg_base,
628 exec_seg_limit,
629 exec_seg_flags,
630 runtime,
631 ident,
632 team_name,
633 code_digests: code_hashes,
634 ..Default::default()
635 };
636
637 for (slot, digest) in special_hashes {
638 cd.set_slot_digest(slot, digest)?;
639 }
640
641 cd.adjust_version(target);
642 cd.clear_newer_fields();
643
644 Ok(cd)
645 }
646
647 pub fn create_special_blobs(
655 &self,
656 settings: &SigningSettings,
657 is_executable: bool,
658 ) -> Result<Vec<(CodeSigningSlot, BlobData<'static>)>, AppleCodesignError> {
659 let mut res = Vec::new();
660
661 let mut requirements = CodeRequirements::default();
662
663 match settings.designated_requirement(SettingsScope::Main) {
664 DesignatedRequirementMode::Auto => {
665 if let Some((_, cert)) = settings.signing_key() {
668 info!("deriving code requirements from signing certificate");
669 let identifier = Some(
670 settings
671 .binary_identifier(SettingsScope::Main)
672 .ok_or(AppleCodesignError::NoIdentifier)?
673 .to_string(),
674 );
675
676 let expr = derive_designated_requirements(
677 cert,
678 settings.certificate_chain(),
679 identifier,
680 )?;
681 requirements.push(expr);
682 }
683 }
684 DesignatedRequirementMode::Explicit(exprs) => {
685 info!("using provided code requirements");
686 for expr in exprs {
687 requirements.push(CodeRequirementExpression::from_bytes(expr)?.0);
688 }
689 }
690 }
691
692 let mut blob = RequirementSetBlob::default();
695
696 if !requirements.is_empty() {
697 requirements.add_to_requirement_set(&mut blob, RequirementType::Designated)?;
698 }
699
700 res.push((CodeSigningSlot::RequirementSet, blob.into()));
701
702 if let Some(entitlements) = settings.entitlements_xml(SettingsScope::Main)? {
703 let blob = EntitlementsBlob::from_string(&entitlements);
704
705 res.push((CodeSigningSlot::Entitlements, blob.into()));
706 }
707
708 if is_executable && let Some(value) = settings.entitlements_plist(SettingsScope::Main) {
714 let blob = EntitlementsDerBlob::from_plist(value)?;
715
716 res.push((CodeSigningSlot::EntitlementsDer, blob.into()));
717 }
718
719 if let Some(constraints) = settings.launch_constraints_self(SettingsScope::Main) {
720 let blob = ConstraintsDerBlob::from_encoded_constraints(constraints)?;
721 res.push((CodeSigningSlot::LaunchConstraintsSelf, blob.into()));
722 }
723
724 if let Some(constraints) = settings.launch_constraints_parent(SettingsScope::Main) {
725 let blob = ConstraintsDerBlob::from_encoded_constraints(constraints)?;
726 res.push((CodeSigningSlot::LaunchConstraintsParent, blob.into()));
727 }
728
729 if let Some(constraints) = settings.launch_constraints_responsible(SettingsScope::Main) {
730 let blob = ConstraintsDerBlob::from_encoded_constraints(constraints)?;
731 res.push((
732 CodeSigningSlot::LaunchConstraintsResponsibleProcess,
733 blob.into(),
734 ));
735 }
736
737 if let Some(constraints) = settings.library_constraints(SettingsScope::Main) {
738 let blob = ConstraintsDerBlob::from_encoded_constraints(constraints)?;
739 res.push((CodeSigningSlot::LibraryConstraints, blob.into()));
740 }
741
742 Ok(res)
743 }
744
745 pub fn estimate_embedded_signature_size(
747 &self,
748 macho: &MachOBinary,
749 settings: &SigningSettings,
750 ) -> Result<usize, AppleCodesignError> {
751 let code_directory_count = 1 + settings
752 .extra_digests(SettingsScope::Main)
753 .map(|x| x.len())
754 .unwrap_or_default();
755
756 let mut size = 1024 * code_directory_count;
758
759 size += macho.code_digests_size(settings.digest_type(SettingsScope::Main), 4096)?;
761
762 if let Some(digests) = settings.extra_digests(SettingsScope::Main) {
763 for digest in digests {
764 size += macho.code_digests_size(*digest, 4096)?;
765 }
766 }
767
768 for (_, blob) in self.create_special_blobs(settings, true)? {
770 size += blob.to_blob_bytes()?.len();
771 }
772
773 size += 4096;
775
776 for cert in settings.certificate_chain() {
778 size += cert.constructed_data().len();
779 }
780
781 size += 1024 - size % 1024;
783
784 Ok(size)
785 }
786}