apple_codesign/
macho.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/*! Mach-O primitives related to code signing
6
7Code signing data is embedded within the named `__LINKEDIT` segment of
8the Mach-O binary. An `LC_CODE_SIGNATURE` load command in the Mach-O header
9will point you at this data. See `find_signature_data()` for this logic.
10
11Within the `__LINKEDIT` segment is a superblob defining embedded signature
12data.
13*/
14
15use {
16    crate::{
17        cryptography::DigestType, embedded_signature::EmbeddedSignature, error::AppleCodesignError,
18    },
19    goblin::mach::{
20        constants::{SEG_LINKEDIT, SEG_TEXT},
21        header::MH_EXECUTE,
22        load_command::{
23            CommandVariant, LinkeditDataCommand, LC_BUILD_VERSION, SIZEOF_LINKEDIT_DATA_COMMAND,
24        },
25        parse_magic_and_ctx,
26        segment::Segment,
27        Mach, MachO, SingleArch,
28    },
29    rayon::prelude::*,
30    scroll::Pread,
31};
32
33/// A Mach-O binary.
34pub struct MachOBinary<'a> {
35    /// Index within a fat binary this Mach-O resides at.
36    ///
37    /// If `None`, this is not inside a fat binary.
38    pub index: Option<usize>,
39
40    /// The parsed Mach-O binary.
41    pub macho: MachO<'a>,
42
43    /// The raw data backing the Mach-O binary.
44    pub data: &'a [u8],
45}
46
47impl<'a> MachOBinary<'a> {
48    /// Parse a non-universal Mach-O binary from raw data.
49    pub fn parse(data: &'a [u8]) -> Result<Self, AppleCodesignError> {
50        let macho = MachO::parse(data, 0)?;
51
52        Ok(Self {
53            index: None,
54            macho,
55            data,
56        })
57    }
58}
59
60impl<'a> MachOBinary<'a> {
61    /// Find the __LINKEDIT segment and its segment index.
62    pub fn linkedit_index_and_segment(&self) -> Option<(usize, &Segment<'a>)> {
63        self.macho
64            .segments
65            .iter()
66            .enumerate()
67            .find(|(_, segment)| matches!(segment.name(), Ok(SEG_LINKEDIT)))
68    }
69
70    /// Find the __LINKEDIT segment.
71    pub fn linkedit_segment(&self) -> Option<&Segment<'a>> {
72        self.linkedit_index_and_segment().map(|(_, x)| x)
73    }
74
75    /// Find the __LINKEDIT segment, asserting it exists and it is the final segment.
76    pub fn linkedit_segment_assert_last(&self) -> Result<&Segment<'a>, AppleCodesignError> {
77        let last_segment = self
78            .segments_by_file_offset()
79            .last()
80            .copied()
81            .ok_or(AppleCodesignError::MissingLinkedit)?;
82
83        if !matches!(last_segment.name(), Ok(SEG_LINKEDIT)) {
84            Err(AppleCodesignError::LinkeditNotLast)
85        } else {
86            Ok(last_segment)
87        }
88    }
89
90    /// Attempt to extract a reference to raw signature data in a Mach-O binary.
91    ///
92    /// An `LC_CODE_SIGNATURE` load command in the Mach-O file header points to
93    /// signature data in the `__LINKEDIT` segment.
94    ///
95    /// This function is used as part of parsing signature data. You probably want to
96    /// use a function that parses referenced data.
97    pub fn find_signature_data(
98        &self,
99    ) -> Result<Option<MachOSignatureData<'a>>, AppleCodesignError> {
100        if let Some(linkedit_data_command) = self.code_signature_load_command() {
101            // Now find the slice of data in the __LINKEDIT segment we need to parse.
102            let (linkedit_segment_index, linkedit) = self
103                .linkedit_index_and_segment()
104                .ok_or(AppleCodesignError::MissingLinkedit)?;
105
106            let linkedit_segment_start_offset = linkedit.fileoff as usize;
107            let linkedit_segment_end_offset = linkedit_segment_start_offset + linkedit.data.len();
108            let signature_file_start_offset = linkedit_data_command.dataoff as usize;
109            let signature_file_end_offset =
110                signature_file_start_offset + linkedit_data_command.datasize as usize;
111            let signature_segment_start_offset =
112                linkedit_data_command.dataoff as usize - linkedit.fileoff as usize;
113            let signature_segment_end_offset =
114                signature_segment_start_offset + linkedit_data_command.datasize as usize;
115
116            let signature_data =
117                &linkedit.data[signature_segment_start_offset..signature_segment_end_offset];
118
119            Ok(Some(MachOSignatureData {
120                linkedit_segment_index,
121                linkedit_segment_start_offset,
122                linkedit_segment_end_offset,
123                signature_file_start_offset,
124                signature_file_end_offset,
125                signature_segment_start_offset,
126                signature_segment_end_offset,
127                linkedit_segment_data: linkedit.data,
128                signature_data,
129            }))
130        } else {
131            Ok(None)
132        }
133    }
134
135    /// Obtain the code signature in the entity.
136    ///
137    /// Returns `Ok(None)` if no signature exists, `Ok(Some)` if it does, or
138    /// `Err` if there is a parse error.
139    pub fn code_signature(&self) -> Result<Option<EmbeddedSignature>, AppleCodesignError> {
140        if let Some(signature) = self.find_signature_data()? {
141            Ok(Some(EmbeddedSignature::from_bytes(
142                signature.signature_data,
143            )?))
144        } else {
145            Ok(None)
146        }
147    }
148
149    /// Determine the start and end offset of the executable segment of a binary.
150    pub fn executable_segment_boundary(&self) -> Result<(u64, u64), AppleCodesignError> {
151        let segment = self
152            .macho
153            .segments
154            .iter()
155            .find(|segment| matches!(segment.name(), Ok(SEG_TEXT)))
156            .ok_or_else(|| AppleCodesignError::InvalidBinary("no __TEXT segment".into()))?;
157
158        Ok((segment.fileoff, segment.fileoff + segment.data.len() as u64))
159    }
160
161    /// Whether this is an executable Mach-O file.
162    pub fn is_executable(&self) -> bool {
163        self.macho.header.filetype == MH_EXECUTE
164    }
165
166    /// The start offset of the code signature data within the __LINKEDIT segment.
167    pub fn code_signature_linkedit_start_offset(&self) -> Option<u32> {
168        let segment = self.linkedit_segment();
169
170        if let (Some(segment), Some(command)) = (segment, self.code_signature_load_command()) {
171            Some((command.dataoff as u64 - segment.fileoff) as u32)
172        } else {
173            None
174        }
175    }
176
177    /// The end offset of the code signature data within the __LINKEDIT segment.
178    pub fn code_signature_linkedit_end_offset(&self) -> Option<u32> {
179        let start_offset = self.code_signature_linkedit_start_offset()?;
180
181        self.code_signature_load_command()
182            .map(|command| start_offset + command.datasize)
183    }
184
185    /// Obtain Mach-O segments by file offset order.
186    ///
187    /// The header-defined order may vary by the file layout order. This ensures the ordering
188    /// is by file layout.
189    pub fn segments_by_file_offset(&self) -> Vec<&Segment<'a>> {
190        let mut segments = self.macho.segments.iter().collect::<Vec<_>>();
191
192        segments.sort_by(|a, b| a.fileoff.cmp(&b.fileoff));
193
194        segments
195    }
196
197    /// The byte offset within the binary at which point "code" stops.
198    ///
199    /// If a signature is present, this is the offset of the start of the
200    /// signature. Else it represents the end of the binary.
201    pub fn code_limit_binary_offset(&self) -> Result<u64, AppleCodesignError> {
202        let last_segment = self.linkedit_segment_assert_last()?;
203
204        if let Some(offset) = self.code_signature_linkedit_start_offset() {
205            Ok(last_segment.fileoff + offset as u64)
206        } else {
207            Ok(last_segment.fileoff + last_segment.data.len() as u64)
208        }
209    }
210
211    /// Obtain __LINKEDIT segment data before the signature data.
212    ///
213    /// If there is no signature, returns all the data for the __LINKEDIT segment.
214    pub fn linkedit_data_before_signature(&self) -> Option<&[u8]> {
215        let segment = self.linkedit_segment();
216
217        if let Some(segment) = segment {
218            if let Some(offset) = self.code_signature_linkedit_start_offset() {
219                Some(&segment.data[0..offset as usize])
220            } else {
221                Some(segment.data)
222            }
223        } else {
224            None
225        }
226    }
227
228    /// Obtain Mach-O binary data to be digested in code digests.
229    ///
230    /// Returns the raw data whose digests will be captured by the Code Directory code digests.
231    pub fn digested_code_data(&self) -> Result<&[u8], AppleCodesignError> {
232        let code_limit = self.code_limit_binary_offset()?;
233
234        Ok(&self.data[0..code_limit as _])
235    }
236
237    /// Obtain the size in bytes of all code digests given a digest type and page size.
238    pub fn code_digests_size(
239        &self,
240        digest: DigestType,
241        page_size: usize,
242    ) -> Result<usize, AppleCodesignError> {
243        let empty = digest.digest_data(b"")?;
244
245        Ok(self.digested_code_data()?.chunks(page_size).count() * empty.len())
246    }
247
248    /// Compute digests over code in this binary.
249    pub fn code_digests(
250        &self,
251        digest: DigestType,
252        page_size: usize,
253    ) -> Result<Vec<Vec<u8>>, AppleCodesignError> {
254        let data = self.digested_code_data()?;
255
256        // Premature parallelism can be slower due to overhead of having to spin up threads.
257        // So only do parallel digests if we have enough data to warrant it.
258        if data.len() > 64 * 1024 * 1024 {
259            data.par_chunks(page_size)
260                .map(|c| digest.digest_data(c))
261                .collect::<Result<Vec<_>, AppleCodesignError>>()
262        } else {
263            self.digested_code_data()?
264                .chunks(page_size)
265                .map(|chunk| digest.digest_data(chunk))
266                .collect::<Result<Vec<_>, AppleCodesignError>>()
267        }
268    }
269
270    /// Resolve the load command for the code signature.
271    pub fn code_signature_load_command(&self) -> Option<LinkeditDataCommand> {
272        self.macho.load_commands.iter().find_map(|lc| {
273            if let CommandVariant::CodeSignature(command) = lc.command {
274                Some(command)
275            } else {
276                None
277            }
278        })
279    }
280
281    /// Attempt to locate embedded Info.plist data.
282    pub fn embedded_info_plist(&self) -> Result<Option<Vec<u8>>, AppleCodesignError> {
283        // Mach-O binaries can have the Info.plist data in an `__info_plist` section
284        // within the __TEXT segment.
285        for segment in &self.macho.segments {
286            if matches!(segment.name(), Ok(SEG_TEXT)) {
287                for (section, data) in segment.sections()? {
288                    if matches!(section.name(), Ok("__info_plist")) {
289                        return Ok(Some(data.to_vec()));
290                    }
291                }
292            }
293        }
294
295        Ok(None)
296    }
297
298    /// Determines whether this crate is capable of signing a given Mach-O binary.
299    ///
300    /// Code in this crate is limited in the amount of Mach-O binary manipulation
301    /// it can perform (supporting rewriting all valid Mach-O binaries effectively
302    /// requires low-level awareness of all Mach-O constructs in order to perform
303    /// offset manipulation). This function can be used to test signing
304    /// compatibility.
305    ///
306    /// We currently only support signing Mach-O files already containing an
307    /// embedded signature. Often linked binaries automatically contain an embedded
308    /// signature containing just the code directory (without a cryptographically
309    /// signed signature), so this limitation hopefully isn't impactful.
310    pub fn check_signing_capability(&self) -> Result<(), AppleCodesignError> {
311        let last_segment = self.linkedit_segment_assert_last()?;
312
313        // Rules:
314        //
315        // 1. If there is an existing signature, there must be no data in
316        //    the binary after it. (We don't know how to update references to
317        //    other data to reflect offset changes.)
318        // 2. If there isn't an existing signature, there must be "room" between
319        //    the last load command and the first section to write a new load
320        //    command for the signature.
321
322        if let Some(offset) = self.code_signature_linkedit_end_offset() {
323            if offset as usize == last_segment.data.len() {
324                Ok(())
325            } else {
326                Err(AppleCodesignError::DataAfterSignature)
327            }
328        } else {
329            let last_load_command = self
330                .macho
331                .load_commands
332                .iter()
333                .last()
334                .ok_or_else(|| AppleCodesignError::InvalidBinary("no load commands".into()))?;
335
336            let first_section = self
337                .macho
338                .segments
339                .iter()
340                .map(|segment| segment.sections())
341                .collect::<Result<Vec<_>, _>>()?
342                .into_iter()
343                .flatten()
344                .next()
345                .ok_or_else(|| AppleCodesignError::InvalidBinary("no sections".into()))?;
346
347            let load_commands_end_offset =
348                last_load_command.offset + last_load_command.command.cmdsize();
349
350            if first_section.0.offset as usize - load_commands_end_offset
351                >= SIZEOF_LINKEDIT_DATA_COMMAND
352            {
353                Ok(())
354            } else {
355                Err(AppleCodesignError::LoadCommandNoRoom)
356            }
357        }
358    }
359
360    /// Attempt to resolve the mach-o targeting settings.
361    pub fn find_targeting(&self) -> Result<Option<MachoTarget>, AppleCodesignError> {
362        let ctx = parse_magic_and_ctx(self.data, 0)?
363            .1
364            .expect("context should have been parsed before");
365
366        for lc in &self.macho.load_commands {
367            if lc.command.cmd() == LC_BUILD_VERSION {
368                let build_version = self
369                    .data
370                    .pread_with::<BuildVersionCommand>(lc.offset, ctx.le)?;
371
372                return Ok(Some(MachoTarget {
373                    platform: build_version.platform.into(),
374                    minimum_os_version: parse_version_nibbles(build_version.minos),
375                    sdk_version: parse_version_nibbles(build_version.sdk),
376                }));
377            }
378        }
379
380        for lc in &self.macho.load_commands {
381            let command = match lc.command {
382                CommandVariant::VersionMinMacosx(c) => Some((c, Platform::MacOs)),
383                CommandVariant::VersionMinIphoneos(c) => Some((c, Platform::IOs)),
384                CommandVariant::VersionMinTvos(c) => Some((c, Platform::TvOs)),
385                CommandVariant::VersionMinWatchos(c) => Some((c, Platform::WatchOs)),
386                _ => None,
387            };
388
389            if let Some((command, platform)) = command {
390                return Ok(Some(MachoTarget {
391                    platform,
392                    minimum_os_version: parse_version_nibbles(command.version),
393                    sdk_version: parse_version_nibbles(command.sdk),
394                }));
395            }
396        }
397
398        Ok(None)
399    }
400}
401
402/// Describes signature data embedded within a Mach-O binary.
403pub struct MachOSignatureData<'a> {
404    /// Which segment offset is the `__LINKEDIT` segment.
405    pub linkedit_segment_index: usize,
406
407    /// Start offset of `__LINKEDIT` segment within the binary.
408    pub linkedit_segment_start_offset: usize,
409
410    /// End offset of `__LINKEDIT` segment within the binary.
411    pub linkedit_segment_end_offset: usize,
412
413    /// Start offset of signature data in `__LINKEDIT` within the binary.
414    pub signature_file_start_offset: usize,
415
416    /// End offset of signature data in `__LINKEDIT` within the binary.
417    pub signature_file_end_offset: usize,
418
419    /// The start offset of the signature data within the `__LINKEDIT` segment.
420    pub signature_segment_start_offset: usize,
421
422    /// The end offset of the signature data within the `__LINKEDIT` segment.
423    pub signature_segment_end_offset: usize,
424
425    /// Raw data in the `__LINKEDIT` segment.
426    pub linkedit_segment_data: &'a [u8],
427
428    /// The signature data within the `__LINKEDIT` segment.
429    pub signature_data: &'a [u8],
430}
431
432/// Content of an `LC_BUILD_VERSION` load command.
433#[derive(Clone, Debug, Pread)]
434pub struct BuildVersionCommand {
435    /// LC_BUILD_VERSION
436    pub cmd: u32,
437    /// Size of load command data.
438    ///
439    /// sizeof(self) + self.ntools * sizeof(BuildToolsVersion)
440    pub cmdsize: u32,
441    /// Platform identifier.
442    pub platform: u32,
443    /// Minimum operating system version.
444    ///
445    /// X.Y.Z encoded in nibbles as xxxx.yy.zz.
446    pub minos: u32,
447    /// SDK version.
448    ///
449    /// X.Y.Z encoded in nibbles as xxxx.yy.zz.
450    pub sdk: u32,
451    /// Number of tools entries following this structure.
452    pub ntools: u32,
453}
454
455/// Represents `PLATFORM_` mach-o constants.
456#[derive(Clone, Copy, Debug, Eq, PartialEq)]
457pub enum Platform {
458    MacOs,
459    IOs,
460    TvOs,
461    WatchOs,
462    BridgeOs,
463    MacCatalyst,
464    IosSimulator,
465    TvOsSimulator,
466    WatchOsSimulator,
467    DriverKit,
468    Unknown(u32),
469}
470
471impl std::fmt::Display for Platform {
472    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
473        match self {
474            Self::MacOs => f.write_str("macOS"),
475            Self::IOs => f.write_str("iOS"),
476            Self::TvOs => f.write_str("tvOS"),
477            Self::WatchOs => f.write_str("watchOS"),
478            Self::BridgeOs => f.write_str("bridgeOS"),
479            Self::MacCatalyst => f.write_str("macCatalyst"),
480            Self::IosSimulator => f.write_str("iOSSimulator"),
481            Self::TvOsSimulator => f.write_str("tvOSSimulator"),
482            Self::WatchOsSimulator => f.write_str("watchOSSimulator"),
483            Self::DriverKit => f.write_str("driverKit"),
484            Self::Unknown(v) => f.write_fmt(format_args!("Unknown ({v})")),
485        }
486    }
487}
488
489impl From<u32> for Platform {
490    fn from(v: u32) -> Self {
491        match v {
492            1 => Self::MacOs,
493            2 => Self::IOs,
494            3 => Self::TvOs,
495            4 => Self::WatchOs,
496            5 => Self::BridgeOs,
497            6 => Self::MacCatalyst,
498            7 => Self::IosSimulator,
499            8 => Self::TvOsSimulator,
500            9 => Self::WatchOsSimulator,
501            10 => Self::DriverKit,
502            _ => Self::Unknown(v),
503        }
504    }
505}
506
507impl From<Platform> for u32 {
508    fn from(val: Platform) -> Self {
509        match val {
510            Platform::MacOs => 1,
511            Platform::IOs => 2,
512            Platform::TvOs => 3,
513            Platform::WatchOs => 4,
514            Platform::BridgeOs => 5,
515            Platform::MacCatalyst => 6,
516            Platform::IosSimulator => 7,
517            Platform::TvOsSimulator => 8,
518            Platform::WatchOsSimulator => 9,
519            Platform::DriverKit => 10,
520            Platform::Unknown(v) => v,
521        }
522    }
523}
524
525impl Platform {
526    /// Resolve SHA-256 digest/signatures support for a given platform type.
527    pub fn sha256_digest_support(&self) -> Result<semver::VersionReq, AppleCodesignError> {
528        let version = match self {
529            // macOS 10.11.4 introduced support for SHA-256.
530            Self::MacOs => ">=10.11.4",
531            // 11.0+ support SHA-256.
532            Self::IOs | Self::TvOs => ">=11.0.0",
533            // WatchOS always uses SHA-1 it appears.
534            Self::WatchOs => ">9999",
535            // Assume no platform needs SHA-1.
536            Self::Unknown(0) => ">9999",
537            // Assume everything else is new and supports SHA-256.
538            _ => "*",
539        };
540
541        Ok(semver::VersionReq::parse(version)?)
542    }
543}
544
545/// Targeting settings for a Mach-O binary.
546pub struct MachoTarget {
547    /// The OS/platform being targeted.
548    pub platform: Platform,
549    /// Minimum required OS version.
550    pub minimum_os_version: semver::Version,
551    /// SDK version targeting.
552    pub sdk_version: semver::Version,
553}
554
555impl MachoTarget {
556    /// Convert the instance to a LC_BUILD_VERSION load command.
557    pub fn to_build_version_command_vec(&self, endian: object::Endianness) -> Vec<u8> {
558        let command = object::macho::BuildVersionCommand {
559            cmd: object::U32::new(endian, object::macho::LC_BUILD_VERSION),
560            cmdsize: object::U32::new(
561                endian,
562                std::mem::size_of::<object::macho::BuildVersionCommand<object::Endianness>>() as _,
563            ),
564            platform: object::U32::new(endian, self.platform.into()),
565            minos: object::U32::new(
566                endian,
567                semver_to_macho_target_version(&self.minimum_os_version),
568            ),
569            sdk: object::U32::new(endian, semver_to_macho_target_version(&self.sdk_version)),
570            ntools: object::U32::new(endian, 0),
571        };
572
573        object::bytes_of(&command).to_vec()
574    }
575}
576
577/// Parses and integer with nibbles xxxx.yy.zz into a [semver::Version].
578pub fn parse_version_nibbles(v: u32) -> semver::Version {
579    let major = v >> 16;
580    let minor = v << 16 >> 24;
581    let patch = v & 0xff;
582
583    semver::Version::new(major as _, minor as _, patch as _)
584}
585
586/// Convert a [semver::Version] to a u32 with nibble encoding used by Mach-O.
587pub fn semver_to_macho_target_version(version: &semver::Version) -> u32 {
588    let major = version.major as u32;
589    let minor = version.minor as u32;
590    let patch = version.patch as u32;
591
592    (major << 16) | ((minor & 0xff) << 8) | (patch & 0xff)
593}
594
595/// Represents a semi-parsed Mach[-O] binary.
596pub struct MachFile<'a> {
597    #[allow(unused)]
598    data: &'a [u8],
599
600    machos: Vec<MachOBinary<'a>>,
601}
602
603impl<'a> MachFile<'a> {
604    /// Construct an instance from data.
605    pub fn parse(data: &'a [u8]) -> Result<Self, AppleCodesignError> {
606        let mach = Mach::parse(data)?;
607
608        let machos = match mach {
609            Mach::Binary(macho) => vec![MachOBinary {
610                index: None,
611                macho,
612                data,
613            }],
614            Mach::Fat(multiarch) => {
615                let mut machos = vec![];
616
617                for (index, arch) in multiarch.arches()?.into_iter().enumerate() {
618                    let macho = match multiarch.get(index)? {
619                        SingleArch::MachO(m) => m,
620                        SingleArch::Archive(_) => continue,
621                    };
622
623                    machos.push(MachOBinary {
624                        index: Some(index),
625                        macho,
626                        data: arch.slice(data),
627                    });
628                }
629
630                machos
631            }
632        };
633
634        Ok(Self { data, machos })
635    }
636
637    /// Whether this Mach-O data has multiple architectures.
638    pub fn is_fat(&self) -> bool {
639        self.machos.len() > 1
640    }
641
642    /// Iterate [MachO] instances in this data.
643    ///
644    /// The `Option<usize>` is `Some` if this is a universal Mach-O or `None` otherwise.
645    pub fn iter_macho(&self) -> impl Iterator<Item = &MachOBinary> {
646        self.machos.iter()
647    }
648
649    pub fn nth_macho(&self, index: usize) -> Result<&MachOBinary<'a>, AppleCodesignError> {
650        self.machos
651            .get(index)
652            .ok_or(AppleCodesignError::InvalidMachOIndex(index))
653    }
654}
655
656impl<'a> IntoIterator for MachFile<'a> {
657    type Item = MachOBinary<'a>;
658    type IntoIter = std::vec::IntoIter<Self::Item>;
659
660    fn into_iter(self) -> Self::IntoIter {
661        self.machos.into_iter()
662    }
663}
664
665#[cfg(test)]
666mod tests {
667    use {
668        super::*,
669        crate::embedded_signature::Blob,
670        std::{
671            io::Read,
672            path::{Path, PathBuf},
673        },
674    };
675
676    const MACHO_UNIVERSAL_MAGIC: [u8; 4] = [0xca, 0xfe, 0xba, 0xbe];
677    const MACHO_64BIT_MAGIC: [u8; 4] = [0xfe, 0xed, 0xfa, 0xcf];
678
679    /// Find files in a directory appearing to be Mach-O by sniffing magic.
680    ///
681    /// Ignores file I/O errors.
682    fn find_likely_macho_files(path: &Path) -> Vec<PathBuf> {
683        let mut res = Vec::new();
684
685        let dir = std::fs::read_dir(path).unwrap();
686
687        for entry in dir {
688            let entry = entry.unwrap();
689
690            if let Ok(mut fh) = std::fs::File::open(entry.path()) {
691                let mut magic = [0; 4];
692
693                if let Ok(size) = fh.read(&mut magic) {
694                    if size == 4 && (magic == MACHO_UNIVERSAL_MAGIC || magic == MACHO_64BIT_MAGIC) {
695                        res.push(entry.path());
696                    }
697                }
698            }
699        }
700
701        res
702    }
703
704    fn find_apple_embedded_signature<'a>(macho: &'a MachOBinary) -> Option<EmbeddedSignature<'a>> {
705        if let Ok(Some(signature)) = macho.code_signature() {
706            Some(signature)
707        } else {
708            None
709        }
710    }
711
712    fn validate_macho(path: &Path, macho: &MachOBinary) {
713        // We found signature data in the binary.
714        if let Some(signature) = find_apple_embedded_signature(macho) {
715            // Attempt a deep parse of all blobs.
716            for blob in &signature.blobs {
717                match blob.clone().into_parsed_blob() {
718                    Ok(parsed) => {
719                        // Attempt to roundtrip the blob data.
720                        match parsed.blob.to_blob_bytes() {
721                            Ok(serialized) => {
722                                if serialized != blob.data {
723                                    println!("blob serialization roundtrip failure on {}: index {}, magic {:?}",
724                                        path.display(),
725                                        blob.index,
726                                        blob.magic,
727                                    );
728                                }
729                            }
730                            Err(e) => {
731                                println!(
732                                    "blob serialization failure on {}; index {}, magic {:?}: {:?}",
733                                    path.display(),
734                                    blob.index,
735                                    blob.magic,
736                                    e
737                                );
738                            }
739                        }
740                    }
741                    Err(e) => {
742                        println!(
743                            "blob parse failure on {}; index {}, magic {:?}: {:?}",
744                            path.display(),
745                            blob.index,
746                            blob.magic,
747                            e
748                        );
749                    }
750                }
751            }
752
753            // Found a CMS signed data blob.
754            if matches!(signature.signature_data(), Ok(Some(_))) {
755                match signature.signed_data() {
756                    Ok(Some(signed_data)) => {
757                        for signer in signed_data.signers() {
758                            if let Err(e) = signer.verify_signature_with_signed_data(&signed_data) {
759                                println!(
760                                    "signature verification failed for {}: {}",
761                                    path.display(),
762                                    e
763                                );
764                            }
765
766                            if let Ok(()) =
767                                signer.verify_message_digest_with_signed_data(&signed_data)
768                            {
769                                println!(
770                                    "message digest verification unexpectedly correct for {}",
771                                    path.display()
772                                );
773                            }
774                        }
775                    }
776                    Ok(None) => {
777                        // This has been observed to occur in the wild. But not from Apple
778                        // signed binaries. Mostly ignore it.
779                        eprintln!(
780                            "{} has a signature blob without CMS data; weird",
781                            path.display()
782                        );
783                    }
784                    Err(e) => {
785                        println!("error performing CMS parse of {}: {:?}", path.display(), e);
786                    }
787                }
788            }
789        }
790    }
791
792    fn validate_macho_in_dir(dir: &Path) {
793        for path in find_likely_macho_files(dir).into_iter() {
794            if let Ok(file_data) = std::fs::read(&path) {
795                if let Ok(mach) = MachFile::parse(&file_data) {
796                    for macho in mach.into_iter() {
797                        validate_macho(&path, &macho);
798                    }
799                }
800            }
801        }
802    }
803
804    #[test]
805    fn parse_applications_macho_signatures() {
806        // This test scans common directories containing Mach-O files on macOS and
807        // verifies we can parse CMS blobs within.
808
809        if let Ok(dir) = std::fs::read_dir("/Applications") {
810            for entry in dir {
811                let entry = entry.unwrap();
812
813                let search_dir = entry.path().join("Contents").join("MacOS");
814
815                if search_dir.exists() {
816                    validate_macho_in_dir(&search_dir);
817                }
818            }
819        }
820
821        for dir in &["/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"] {
822            let dir = PathBuf::from(dir);
823
824            if dir.exists() {
825                validate_macho_in_dir(&dir);
826            }
827        }
828    }
829
830    #[test]
831    fn version_nibbles() {
832        assert_eq!(
833            parse_version_nibbles(12 << 16 | 1 << 8 | 2),
834            semver::Version::new(12, 1, 2)
835        );
836        assert_eq!(
837            parse_version_nibbles(11 << 16 | 10 << 8 | 15),
838            semver::Version::new(11, 10, 15)
839        );
840        assert_eq!(
841            semver_to_macho_target_version(&semver::Version::new(12, 1, 2)),
842            12 << 16 | 1 << 8 | 2
843        );
844    }
845}