Skip to main content

isideload_apple_codesign/
reader.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//! Functionality for reading signature data from files.
6
7use {
8    crate::{
9        certificate::AppleCertificate,
10        code_directory::CodeDirectoryBlob,
11        cryptography::DigestType,
12        dmg::{DmgReader, path_is_dmg},
13        embedded_signature::{BlobEntry, EmbeddedSignature},
14        embedded_signature_builder::{CD_DIGESTS_OID, CD_DIGESTS_PLIST_OID},
15        error::{AppleCodesignError, Result},
16        macho::{MachFile, MachOBinary},
17    },
18    apple_bundles::{DirectoryBundle, DirectoryBundleFile},
19    apple_xar::{
20        reader::XarReader,
21        table_of_contents::{
22            ChecksumType as XarChecksumType, File as XarTocFile, Signature as XarTocSignature,
23        },
24    },
25    cryptographic_message_syntax::{SignedData, SignerInfo},
26    goblin::mach::{fat::FAT_MAGIC, parse_magic_and_ctx},
27    isideload_vfs::fs::File,
28    serde::Serialize,
29    std::{
30        fmt::Debug,
31        io::{BufWriter, Cursor, Read, Seek},
32        ops::Deref,
33        path::{Path, PathBuf},
34    },
35    x509_certificate::{CapturedX509Certificate, DigestAlgorithm},
36};
37
38enum MachOType {
39    Mach,
40    MachO,
41}
42
43impl MachOType {
44    pub fn from_path(path: impl AsRef<Path>) -> Result<Option<Self>, AppleCodesignError> {
45        let mut fh = File::open(path.as_ref())?;
46
47        let mut header = vec![0u8; 4];
48        let count = fh.read(&mut header)?;
49
50        if count < 4 {
51            return Ok(None);
52        }
53
54        let magic = goblin::mach::peek(&header, 0)?;
55
56        if magic == FAT_MAGIC {
57            Ok(Some(Self::Mach))
58        } else if let Ok((_, Some(_))) = parse_magic_and_ctx(&header, 0) {
59            Ok(Some(Self::MachO))
60        } else {
61            Ok(None)
62        }
63    }
64}
65
66/// Test whether a given path is likely a XAR file.
67pub fn path_is_xar(path: impl AsRef<Path>) -> Result<bool, AppleCodesignError> {
68    let mut fh = File::open(path.as_ref())?;
69
70    let mut header = [0u8; 4];
71
72    let count = fh.read(&mut header)?;
73    if count < 4 {
74        Ok(false)
75    } else {
76        Ok(header.as_ref() == b"xar!")
77    }
78}
79
80/// Test whether a given path is likely a ZIP file.
81pub fn path_is_zip(path: impl AsRef<Path>) -> Result<bool, AppleCodesignError> {
82    let mut fh = File::open(path.as_ref())?;
83
84    let mut header = [0u8; 4];
85
86    let count = fh.read(&mut header)?;
87    if count < 4 {
88        Ok(false)
89    } else {
90        Ok(header.as_ref() == [0x50, 0x4b, 0x03, 0x04])
91    }
92}
93
94/// Whether the specified filesystem path is a Mach-O binary.
95pub fn path_is_macho(path: impl AsRef<Path>) -> Result<bool, AppleCodesignError> {
96    Ok(MachOType::from_path(path)?.is_some())
97}
98
99/// Describes the type of entity at a path.
100///
101/// This represents a best guess.
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum PathType {
104    MachO,
105    Dmg,
106    Bundle,
107    Xar,
108    Zip,
109    Other,
110}
111
112impl PathType {
113    /// Attempt to classify the type of signable entity based on a filesystem path.
114    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, AppleCodesignError> {
115        let path = path.as_ref();
116        let meta = isideload_vfs::fs::metadata(path)?;
117
118        if meta.is_file() {
119            if path_is_dmg(path)? {
120                Ok(Self::Dmg)
121            } else if path_is_xar(path)? {
122                Ok(Self::Xar)
123            } else if path_is_zip(path)? {
124                Ok(Self::Zip)
125            } else if path_is_macho(path)? {
126                Ok(Self::MachO)
127            } else {
128                Ok(Self::Other)
129            }
130        } else if meta.is_dir() {
131            Ok(Self::Bundle)
132        } else {
133            Ok(Self::Other)
134        }
135    }
136}
137
138fn format_integer<T: std::fmt::Display + std::fmt::LowerHex>(v: T) -> String {
139    format!("{} / 0x{:x}", v, v)
140}
141
142fn pretty_print_xml(xml: &[u8]) -> Result<Vec<u8>, AppleCodesignError> {
143    let mut reader = xml::reader::EventReader::new(Cursor::new(xml));
144    let mut emitter = xml::EmitterConfig::new()
145        .perform_indent(true)
146        .create_writer(BufWriter::new(Vec::with_capacity(xml.len() * 2)));
147
148    while let Ok(event) = reader.next() {
149        match event {
150            xml::reader::XmlEvent::EndDocument => {
151                break;
152            }
153            xml::reader::XmlEvent::Whitespace(_) => {}
154            event => {
155                if let Some(event) = event.as_writer_event() {
156                    emitter.write(event).map_err(AppleCodesignError::XmlWrite)?;
157                }
158            }
159        }
160    }
161
162    let xml = emitter.into_inner().into_inner().map_err(|e| {
163        AppleCodesignError::Io(std::io::Error::new(std::io::ErrorKind::BrokenPipe, e))
164    })?;
165
166    Ok(xml)
167}
168
169/// Pretty print XML and turn into a Vec of lines.
170fn pretty_print_xml_lines(xml: &[u8]) -> Result<Vec<String>> {
171    Ok(String::from_utf8_lossy(pretty_print_xml(xml)?.as_ref())
172        .lines()
173        .map(|x| x.to_string())
174        .collect::<Vec<_>>())
175}
176
177#[derive(Clone, Debug, Serialize)]
178pub struct BlobDescription {
179    pub slot: String,
180    pub magic: String,
181    pub length: u32,
182    pub sha1: String,
183    pub sha256: String,
184}
185
186impl<'a> From<&BlobEntry<'a>> for BlobDescription {
187    fn from(entry: &BlobEntry<'a>) -> Self {
188        Self {
189            slot: format!("{:?}", entry.slot),
190            magic: format!("{:x}", u32::from(entry.magic)),
191            length: entry.length as _,
192            sha1: hex::encode(
193                entry
194                    .digest_with(DigestType::Sha1)
195                    .expect("sha-1 digest should always work"),
196            ),
197            sha256: hex::encode(
198                entry
199                    .digest_with(DigestType::Sha256)
200                    .expect("sha-256 digest should always work"),
201            ),
202        }
203    }
204}
205
206#[derive(Clone, Debug, Serialize)]
207pub struct CertificateInfo {
208    pub subject: String,
209    pub issuer: String,
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub key_algorithm: Option<String>,
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub signature_algorithm: Option<String>,
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub signed_with_algorithm: Option<String>,
216    pub is_apple_root_ca: bool,
217    pub is_apple_intermediate_ca: bool,
218    pub chains_to_apple_root_ca: bool,
219    #[serde(skip_serializing_if = "Vec::is_empty")]
220    pub apple_ca_extensions: Vec<String>,
221    #[serde(skip_serializing_if = "Vec::is_empty")]
222    pub apple_extended_key_usages: Vec<String>,
223    #[serde(skip_serializing_if = "Vec::is_empty")]
224    pub apple_code_signing_extensions: Vec<String>,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub apple_certificate_profile: Option<String>,
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub apple_team_id: Option<String>,
229}
230
231impl TryFrom<&CapturedX509Certificate> for CertificateInfo {
232    type Error = AppleCodesignError;
233
234    fn try_from(cert: &CapturedX509Certificate) -> Result<Self, Self::Error> {
235        Ok(Self {
236            subject: cert
237                .subject_name()
238                .user_friendly_str()
239                .map_err(AppleCodesignError::CertificateDecode)?,
240            issuer: cert
241                .issuer_name()
242                .user_friendly_str()
243                .map_err(AppleCodesignError::CertificateDecode)?,
244            key_algorithm: cert.key_algorithm().map(|x| x.to_string()),
245            signature_algorithm: cert.signature_algorithm().map(|x| x.to_string()),
246            signed_with_algorithm: cert.signature_signature_algorithm().map(|x| x.to_string()),
247            is_apple_root_ca: cert.is_apple_root_ca(),
248            is_apple_intermediate_ca: cert.is_apple_intermediate_ca(),
249            chains_to_apple_root_ca: cert.chains_to_apple_root_ca(),
250            apple_ca_extensions: cert
251                .apple_ca_extensions()
252                .into_iter()
253                .map(|x| x.to_string())
254                .collect::<Vec<_>>(),
255            apple_extended_key_usages: cert
256                .apple_extended_key_usage_purposes()
257                .into_iter()
258                .map(|x| x.to_string())
259                .collect::<Vec<_>>(),
260            apple_code_signing_extensions: cert
261                .apple_code_signing_extensions()
262                .into_iter()
263                .map(|x| x.to_string())
264                .collect::<Vec<_>>(),
265            apple_certificate_profile: cert.apple_guess_profile().map(|x| x.to_string()),
266            apple_team_id: cert.apple_team_id(),
267        })
268    }
269}
270
271#[derive(Clone, Debug, Serialize)]
272pub struct CmsSigner {
273    pub issuer: String,
274    pub digest_algorithm: String,
275    pub signature_algorithm: String,
276    #[serde(skip_serializing_if = "Vec::is_empty")]
277    pub attributes: Vec<String>,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub content_type: Option<String>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub message_digest: Option<String>,
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub signing_time: Option<chrono::DateTime<chrono::Utc>>,
284    #[serde(skip_serializing_if = "Vec::is_empty")]
285    pub cdhash_plist: Vec<String>,
286    #[serde(skip_serializing_if = "Vec::is_empty")]
287    pub cdhash_digests: Vec<(String, String)>,
288    pub signature_verifies: bool,
289}
290
291impl CmsSigner {
292    pub fn from_signer_info_and_signed_data(
293        signer_info: &SignerInfo,
294        signed_data: &SignedData,
295    ) -> Result<Self, AppleCodesignError> {
296        let mut attributes = vec![];
297        let mut content_type = None;
298        let mut message_digest = None;
299        let mut signing_time = None;
300        let mut cdhash_plist = vec![];
301        let mut cdhash_digests = vec![];
302
303        if let Some(sa) = signer_info.signed_attributes() {
304            content_type = Some(sa.content_type().to_string());
305            message_digest = Some(hex::encode(sa.message_digest()));
306            if let Some(t) = sa.signing_time() {
307                signing_time = Some(*t);
308            }
309
310            for attr in sa.attributes().iter() {
311                attributes.push(format!("{}", attr.typ));
312
313                if attr.typ == CD_DIGESTS_PLIST_OID {
314                    if let Some(data) = attr.values.first() {
315                        let data = data.deref().clone();
316
317                        let plist = data
318                            .decode(|cons| {
319                                let v = bcder::OctetString::take_from(cons)?;
320
321                                Ok(v.into_bytes())
322                            })
323                            .map_err(|e| AppleCodesignError::Cms(e.into()))?;
324
325                        cdhash_plist = pretty_print_xml_lines(&plist)?;
326                    }
327                } else if attr.typ == CD_DIGESTS_OID {
328                    for value in &attr.values {
329                        // Each value is a SEQUENECE of (OID, OctetString).
330                        let data = value.deref().clone();
331
332                        data.decode(|cons| {
333                            loop {
334                                let res = cons.take_opt_sequence(|cons| {
335                                    let oid = bcder::Oid::take_from(cons)?;
336                                    let value = bcder::OctetString::take_from(cons)?;
337
338                                    cdhash_digests
339                                        .push((format!("{oid}"), hex::encode(value.into_bytes())));
340
341                                    Ok(())
342                                })?;
343
344                                if res.is_none() {
345                                    break;
346                                }
347                            }
348
349                            Ok(())
350                        })
351                        .map_err(|e| AppleCodesignError::Cms(e.into()))?;
352                    }
353                }
354            }
355        }
356
357        // The order should matter per RFC 5652 but Apple's CMS implementation doesn't
358        // conform to spec.
359        attributes.sort();
360
361        Ok(Self {
362            issuer: signer_info
363                .certificate_issuer_and_serial()
364                .expect("issuer should always be set")
365                .0
366                .user_friendly_str()
367                .map_err(AppleCodesignError::CertificateDecode)?,
368            digest_algorithm: signer_info.digest_algorithm().to_string(),
369            signature_algorithm: signer_info.signature_algorithm().to_string(),
370            attributes,
371            content_type,
372            message_digest,
373            signing_time,
374            cdhash_plist,
375            cdhash_digests,
376            signature_verifies: signer_info
377                .verify_signature_with_signed_data(signed_data)
378                .is_ok(),
379        })
380    }
381}
382
383/// High-level representation of a CMS signature.
384#[derive(Clone, Debug, Serialize)]
385pub struct CmsSignature {
386    #[serde(skip_serializing_if = "Vec::is_empty")]
387    pub certificates: Vec<CertificateInfo>,
388    #[serde(skip_serializing_if = "Vec::is_empty")]
389    pub signers: Vec<CmsSigner>,
390}
391
392impl TryFrom<SignedData> for CmsSignature {
393    type Error = AppleCodesignError;
394
395    fn try_from(signed_data: SignedData) -> Result<Self, Self::Error> {
396        let certificates = signed_data
397            .certificates()
398            .map(|x| x.try_into())
399            .collect::<Result<Vec<_>, _>>()?;
400
401        let signers = signed_data
402            .signers()
403            .map(|x| CmsSigner::from_signer_info_and_signed_data(x, &signed_data))
404            .collect::<Result<Vec<_>, _>>()?;
405
406        Ok(Self {
407            certificates,
408            signers,
409        })
410    }
411}
412
413#[derive(Clone, Debug, Serialize)]
414pub struct CodeDirectory {
415    pub version: String,
416    pub flags: String,
417    pub identifier: String,
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub team_name: Option<String>,
420    pub digest_type: String,
421    pub platform: u8,
422    pub signed_entity_size: u64,
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub executable_segment_flags: Option<String>,
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub runtime_version: Option<String>,
427    pub code_digests_count: usize,
428    #[serde(skip_serializing_if = "Vec::is_empty")]
429    slot_digests: Vec<String>,
430}
431
432impl<'a> TryFrom<CodeDirectoryBlob<'a>> for CodeDirectory {
433    type Error = AppleCodesignError;
434
435    fn try_from(cd: CodeDirectoryBlob<'a>) -> Result<Self, Self::Error> {
436        let mut temp = cd
437            .slot_digests()
438            .iter()
439            .map(|(slot, digest)| (slot, digest.as_hex()))
440            .collect::<Vec<_>>();
441        temp.sort_by_key(|(a, _)| *a);
442
443        let slot_digests = temp
444            .into_iter()
445            .map(|(slot, digest)| format!("{slot:?}: {digest}"))
446            .collect::<Vec<_>>();
447
448        Ok(Self {
449            version: format!("0x{:X}", cd.version),
450            flags: format!("{:?}", cd.flags),
451            identifier: cd.ident.to_string(),
452            team_name: cd.team_name.map(|x| x.to_string()),
453            signed_entity_size: cd.code_limit as _,
454            digest_type: format!("{}", cd.digest_type),
455            platform: cd.platform,
456            executable_segment_flags: cd.exec_seg_flags.map(|x| format!("{x:?}")),
457            runtime_version: cd
458                .runtime
459                .map(|x| format!("{}", crate::macho::parse_version_nibbles(x))),
460            code_digests_count: cd.code_digests.len(),
461            slot_digests,
462        })
463    }
464}
465
466/// High level representation of a code signature.
467#[derive(Clone, Debug, Serialize)]
468pub struct CodeSignature {
469    /// Length of the code signature data.
470    pub superblob_length: String,
471    pub blob_count: u32,
472    pub blobs: Vec<BlobDescription>,
473    #[serde(skip_serializing_if = "Option::is_none")]
474    pub code_directory: Option<CodeDirectory>,
475    #[serde(skip_serializing_if = "Vec::is_empty")]
476    pub alternative_code_directories: Vec<(String, CodeDirectory)>,
477    #[serde(skip_serializing_if = "Vec::is_empty")]
478    pub entitlements_plist: Vec<String>,
479    #[serde(skip_serializing_if = "Vec::is_empty")]
480    pub entitlements_der_plist: Vec<String>,
481    #[serde(skip_serializing_if = "Vec::is_empty")]
482    pub launch_constraints_self: Vec<String>,
483    #[serde(skip_serializing_if = "Vec::is_empty")]
484    pub launch_constraints_parent: Vec<String>,
485    #[serde(skip_serializing_if = "Vec::is_empty")]
486    pub launch_constraints_responsible: Vec<String>,
487    #[serde(skip_serializing_if = "Vec::is_empty")]
488    pub library_constraints: Vec<String>,
489    #[serde(skip_serializing_if = "Vec::is_empty")]
490    pub code_requirements: Vec<String>,
491    pub cms: Option<CmsSignature>,
492}
493
494impl<'a> TryFrom<EmbeddedSignature<'a>> for CodeSignature {
495    type Error = AppleCodesignError;
496
497    fn try_from(sig: EmbeddedSignature<'a>) -> Result<Self, Self::Error> {
498        let mut entitlements_plist = vec![];
499        let mut entitlements_der_plist = vec![];
500        let mut launch_constraints_self = vec![];
501        let mut launch_constraints_parent = vec![];
502        let mut launch_constraints_responsible = vec![];
503        let mut library_constraints = vec![];
504        let mut code_requirements = vec![];
505        let mut cms = None;
506
507        let code_directory = if let Some(cd) = sig.code_directory()? {
508            Some(CodeDirectory::try_from(*cd)?)
509        } else {
510            None
511        };
512
513        let alternative_code_directories = sig
514            .alternate_code_directories()?
515            .into_iter()
516            .map(|(slot, cd)| Ok((format!("{slot:?}"), CodeDirectory::try_from(*cd)?)))
517            .collect::<Result<Vec<_>, AppleCodesignError>>()?;
518
519        if let Some(blob) = sig.entitlements()? {
520            entitlements_plist = blob
521                .as_str()
522                .lines()
523                .map(|x| x.replace('\t', "  "))
524                .collect::<Vec<_>>();
525        }
526
527        if let Some(blob) = sig.entitlements_der()? {
528            let xml = blob.plist_xml()?;
529
530            entitlements_der_plist = pretty_print_xml_lines(&xml)?;
531        }
532
533        if let Some(blob) = sig.launch_constraints_self()? {
534            launch_constraints_self = pretty_print_xml_lines(&blob.plist_xml()?)?;
535        }
536
537        if let Some(blob) = sig.launch_constraints_parent()? {
538            launch_constraints_parent = pretty_print_xml_lines(&blob.plist_xml()?)?;
539        }
540
541        if let Some(blob) = sig.launch_constraints_responsible()? {
542            launch_constraints_responsible = pretty_print_xml_lines(&blob.plist_xml()?)?;
543        }
544
545        if let Some(blob) = sig.library_constraints()? {
546            library_constraints = pretty_print_xml_lines(&blob.plist_xml()?)?;
547        }
548
549        if let Some(req) = sig.code_requirements()? {
550            let mut temp = vec![];
551
552            for (req, blob) in req.requirements {
553                let reqs = blob.parse_expressions()?;
554                temp.push((req, format!("{reqs}")));
555            }
556
557            temp.sort_by_key(|(a, _)| *a);
558
559            code_requirements = temp
560                .into_iter()
561                .map(|(req, value)| format!("{req}: {value}"))
562                .collect::<Vec<_>>();
563        }
564
565        if let Some(signed_data) = sig.signed_data()? {
566            cms = Some(signed_data.try_into()?);
567        }
568
569        Ok(Self {
570            superblob_length: format_integer(sig.length),
571            blob_count: sig.count,
572            blobs: sig
573                .blobs
574                .iter()
575                .map(BlobDescription::from)
576                .collect::<Vec<_>>(),
577            code_directory,
578            alternative_code_directories,
579            entitlements_plist,
580            entitlements_der_plist,
581            launch_constraints_self,
582            launch_constraints_parent,
583            launch_constraints_responsible,
584            library_constraints,
585            code_requirements,
586            cms,
587        })
588    }
589}
590
591#[derive(Clone, Debug, Default, Serialize)]
592pub struct MachOEntity {
593    pub macho_linkedit_start_offset: Option<String>,
594    pub macho_signature_start_offset: Option<String>,
595    pub macho_signature_end_offset: Option<String>,
596    pub macho_linkedit_end_offset: Option<String>,
597    pub macho_end_offset: Option<String>,
598    pub linkedit_signature_start_offset: Option<String>,
599    pub linkedit_signature_end_offset: Option<String>,
600    pub linkedit_bytes_after_signature: Option<String>,
601    pub signature: Option<CodeSignature>,
602}
603
604#[derive(Clone, Debug, Serialize)]
605pub struct DmgEntity {
606    pub code_signature_offset: u64,
607    pub code_signature_size: u64,
608    pub signature: Option<CodeSignature>,
609}
610
611#[derive(Clone, Debug, Serialize)]
612pub enum CodeSignatureFile {
613    ResourcesXml(Vec<String>),
614    NotarizationTicket,
615    Other,
616}
617
618#[derive(Clone, Debug, Serialize)]
619pub struct XarTableOfContents {
620    pub toc_length_compressed: u64,
621    pub toc_length_uncompressed: u64,
622    pub checksum_offset: u64,
623    pub checksum_size: u64,
624    pub checksum_type: String,
625    pub toc_start_offset: u16,
626    pub heap_start_offset: u64,
627    pub creation_time: String,
628    pub toc_checksum_reported: String,
629    pub toc_checksum_reported_sha1_digest: String,
630    pub toc_checksum_reported_sha256_digest: String,
631    pub toc_checksum_actual_sha1: String,
632    pub toc_checksum_actual_sha256: String,
633    pub checksum_verifies: bool,
634    #[serde(skip_serializing_if = "Option::is_none")]
635    pub signature: Option<XarSignature>,
636    #[serde(skip_serializing_if = "Option::is_none")]
637    pub x_signature: Option<XarSignature>,
638    #[serde(skip_serializing_if = "Vec::is_empty")]
639    pub xml: Vec<String>,
640    #[serde(skip_serializing_if = "Option::is_none")]
641    pub rsa_signature: Option<String>,
642    #[serde(skip_serializing_if = "Option::is_none")]
643    pub rsa_signature_verifies: Option<bool>,
644    #[serde(skip_serializing_if = "Option::is_none")]
645    pub cms_signature: Option<CmsSignature>,
646    #[serde(skip_serializing_if = "Option::is_none")]
647    pub cms_signature_verifies: Option<bool>,
648}
649
650impl XarTableOfContents {
651    pub fn from_xar<R: Read + Seek + Sized + Debug>(
652        xar: &mut XarReader<R>,
653    ) -> Result<Self, AppleCodesignError> {
654        let (digest_type, digest) = xar.checksum()?;
655        let _xml = xar.table_of_contents_decoded_data()?;
656
657        let (rsa_signature, rsa_signature_verifies) = if let Some(sig) = xar.rsa_signature()? {
658            (
659                Some(hex::encode(sig.0)),
660                Some(xar.verify_rsa_checksum_signature().unwrap_or(false)),
661            )
662        } else {
663            (None, None)
664        };
665        let (cms_signature, cms_signature_verifies) =
666            if let Some(signed_data) = xar.cms_signature()? {
667                (
668                    Some(CmsSignature::try_from(signed_data)?),
669                    Some(xar.verify_cms_signature().unwrap_or(false)),
670                )
671            } else {
672                (None, None)
673            };
674
675        let toc_checksum_actual_sha1 = xar.digest_table_of_contents_with(XarChecksumType::Sha1)?;
676        let toc_checksum_actual_sha256 =
677            xar.digest_table_of_contents_with(XarChecksumType::Sha256)?;
678
679        let checksum_verifies = xar.verify_table_of_contents_checksum().unwrap_or(false);
680
681        let header = xar.header();
682        let toc = xar.table_of_contents();
683        let checksum_offset = toc.checksum.offset;
684        let checksum_size = toc.checksum.size;
685
686        // This can be useful for debugging.
687        //let xml = pretty_print_xml_lines(&xml)?;
688        let xml = vec![];
689
690        Ok(Self {
691            toc_length_compressed: header.toc_length_compressed,
692            toc_length_uncompressed: header.toc_length_uncompressed,
693            checksum_offset,
694            checksum_size,
695            checksum_type: apple_xar::format::XarChecksum::from(header.checksum_algorithm_id)
696                .to_string(),
697            toc_start_offset: header.size,
698            heap_start_offset: xar.heap_start_offset(),
699            creation_time: toc.creation_time.clone(),
700            toc_checksum_reported: format!("{}:{}", digest_type, hex::encode(&digest)),
701            toc_checksum_reported_sha1_digest: hex::encode(DigestType::Sha1.digest_data(&digest)?),
702            toc_checksum_reported_sha256_digest: hex::encode(
703                DigestType::Sha256.digest_data(&digest)?,
704            ),
705            toc_checksum_actual_sha1: hex::encode(toc_checksum_actual_sha1),
706            toc_checksum_actual_sha256: hex::encode(toc_checksum_actual_sha256),
707            checksum_verifies,
708            signature: if let Some(sig) = &toc.signature {
709                Some(sig.try_into()?)
710            } else {
711                None
712            },
713            x_signature: if let Some(sig) = &toc.x_signature {
714                Some(sig.try_into()?)
715            } else {
716                None
717            },
718            xml,
719            rsa_signature,
720            rsa_signature_verifies,
721            cms_signature,
722            cms_signature_verifies,
723        })
724    }
725}
726
727#[derive(Clone, Debug, Serialize)]
728pub struct XarSignature {
729    pub style: String,
730    pub offset: u64,
731    pub size: u64,
732    pub end_offset: u64,
733    #[serde(skip_serializing_if = "Vec::is_empty")]
734    pub certificates: Vec<CertificateInfo>,
735}
736
737impl TryFrom<&XarTocSignature> for XarSignature {
738    type Error = AppleCodesignError;
739
740    fn try_from(sig: &XarTocSignature) -> Result<Self, Self::Error> {
741        Ok(Self {
742            style: sig.style.to_string(),
743            offset: sig.offset,
744            size: sig.size,
745            end_offset: sig.offset + sig.size,
746            certificates: sig
747                .x509_certificates()?
748                .into_iter()
749                .map(|cert| CertificateInfo::try_from(&cert))
750                .collect::<Result<Vec<_>, AppleCodesignError>>()?,
751        })
752    }
753}
754
755#[derive(Clone, Debug, Default, Serialize)]
756pub struct XarFile {
757    pub id: u64,
758    pub file_type: String,
759    pub data_size: Option<u64>,
760    pub data_length: Option<u64>,
761    pub data_extracted_checksum: Option<String>,
762    pub data_archived_checksum: Option<String>,
763    pub data_encoding: Option<String>,
764}
765
766impl TryFrom<&XarTocFile> for XarFile {
767    type Error = AppleCodesignError;
768
769    fn try_from(file: &XarTocFile) -> Result<Self, Self::Error> {
770        let mut v = Self {
771            id: file.id,
772            file_type: file.file_type.to_string(),
773            ..Default::default()
774        };
775
776        if let Some(data) = &file.data {
777            v.populate_data(data);
778        }
779
780        Ok(v)
781    }
782}
783
784impl XarFile {
785    pub fn populate_data(&mut self, data: &apple_xar::table_of_contents::FileData) {
786        self.data_size = Some(data.size);
787        self.data_length = Some(data.length);
788        self.data_extracted_checksum = Some(format!(
789            "{}:{}",
790            data.extracted_checksum.style, data.extracted_checksum.checksum
791        ));
792        self.data_archived_checksum = Some(format!(
793            "{}:{}",
794            data.archived_checksum.style, data.archived_checksum.checksum
795        ));
796        self.data_encoding = Some(data.encoding.style.clone());
797    }
798}
799
800#[derive(Clone, Debug, Serialize)]
801#[serde(rename_all = "snake_case")]
802pub enum SignatureEntity {
803    MachO(MachOEntity),
804    Dmg(DmgEntity),
805    BundleCodeSignatureFile(CodeSignatureFile),
806    XarTableOfContents(XarTableOfContents),
807    XarMember(XarFile),
808    Other,
809}
810
811#[derive(Clone, Debug, Serialize)]
812pub struct FileEntity {
813    pub path: PathBuf,
814    #[serde(skip_serializing_if = "Option::is_none")]
815    pub file_size: Option<u64>,
816    #[serde(skip_serializing_if = "Option::is_none")]
817    pub file_sha256: Option<String>,
818    #[serde(skip_serializing_if = "Option::is_none")]
819    pub symlink_target: Option<PathBuf>,
820    #[serde(skip_serializing_if = "Option::is_none")]
821    pub sub_path: Option<String>,
822    #[serde(with = "serde_yaml::with::singleton_map")]
823    pub entity: SignatureEntity,
824}
825
826impl FileEntity {
827    /// Construct an instance from a [Path].
828    pub fn from_path(path: &Path, report_path: Option<&Path>) -> Result<Self, AppleCodesignError> {
829        let metadata = isideload_vfs::fs::symlink_metadata(path)?;
830
831        let report_path = if let Some(p) = report_path {
832            p.to_path_buf()
833        } else {
834            path.to_path_buf()
835        };
836
837        let (file_size, file_sha256, symlink_target) = if metadata.is_symlink() {
838            (None, None, Some(isideload_vfs::fs::read_link(path)?))
839        } else {
840            (
841                Some(metadata.len()),
842                Some(hex::encode(DigestAlgorithm::Sha256.digest_path(path)?)),
843                None,
844            )
845        };
846
847        Ok(Self {
848            path: report_path,
849            file_size,
850            file_sha256,
851            symlink_target,
852            sub_path: None,
853            entity: SignatureEntity::Other,
854        })
855    }
856}
857
858/// Entity for reading Apple code signature data.
859pub enum SignatureReader {
860    Dmg(PathBuf, Box<DmgReader>),
861    MachO(PathBuf, Vec<u8>),
862    Bundle(Box<DirectoryBundle>),
863    FlatPackage(PathBuf),
864}
865
866impl SignatureReader {
867    /// Construct a signature reader from a path.
868    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, AppleCodesignError> {
869        let path = path.as_ref();
870        match PathType::from_path(path)? {
871            PathType::Bundle => Ok(Self::Bundle(Box::new(
872                DirectoryBundle::new_from_path(path)
873                    .map_err(AppleCodesignError::DirectoryBundle)?,
874            ))),
875            PathType::Dmg => {
876                let mut fh = File::open(path)?;
877                Ok(Self::Dmg(
878                    path.to_path_buf(),
879                    Box::new(DmgReader::new(&mut fh)?),
880                ))
881            }
882            PathType::MachO => {
883                let data = isideload_vfs::fs::read(path)?;
884                MachFile::parse(&data)?;
885
886                Ok(Self::MachO(path.to_path_buf(), data))
887            }
888            PathType::Xar => Ok(Self::FlatPackage(path.to_path_buf())),
889            PathType::Zip | PathType::Other => Err(AppleCodesignError::UnrecognizedPathType),
890        }
891    }
892
893    /// Obtain entities that are possibly relevant to code signing.
894    pub fn entities(&self) -> Result<Vec<FileEntity>, AppleCodesignError> {
895        match self {
896            Self::Dmg(path, dmg) => {
897                let mut entity = FileEntity::from_path(path, None)?;
898                entity.entity = SignatureEntity::Dmg(Self::resolve_dmg_entity(dmg)?);
899
900                Ok(vec![entity])
901            }
902            Self::MachO(path, data) => Self::resolve_macho_entities_from_data(path, data, None),
903            Self::Bundle(bundle) => Self::resolve_bundle_entities(bundle),
904            Self::FlatPackage(path) => Self::resolve_flat_package_entities(path),
905        }
906    }
907
908    fn resolve_dmg_entity(dmg: &DmgReader) -> Result<DmgEntity, AppleCodesignError> {
909        let signature = if let Some(sig) = dmg.embedded_signature()? {
910            Some(sig.try_into()?)
911        } else {
912            None
913        };
914
915        Ok(DmgEntity {
916            code_signature_offset: dmg.koly().code_signature_offset,
917            code_signature_size: dmg.koly().code_signature_size,
918            signature,
919        })
920    }
921
922    fn resolve_macho_entities_from_data(
923        path: &Path,
924        data: &[u8],
925        report_path: Option<&Path>,
926    ) -> Result<Vec<FileEntity>, AppleCodesignError> {
927        let mut entities = vec![];
928
929        let entity = FileEntity::from_path(path, report_path)?;
930
931        for macho in MachFile::parse(data)?.into_iter() {
932            let mut entity = entity.clone();
933
934            if let Some(index) = macho.index {
935                entity.sub_path = Some(format!("macho-index:{index}"));
936            }
937
938            entity.entity = SignatureEntity::MachO(Self::resolve_macho_entity(macho)?);
939
940            entities.push(entity);
941        }
942
943        Ok(entities)
944    }
945
946    fn resolve_macho_entity(macho: MachOBinary) -> Result<MachOEntity, AppleCodesignError> {
947        let mut entity = MachOEntity::default();
948
949        entity.macho_end_offset = Some(format_integer(macho.data.len()));
950
951        if let Some(sig) = macho.find_signature_data()? {
952            entity.macho_linkedit_start_offset =
953                Some(format_integer(sig.linkedit_segment_start_offset));
954            entity.macho_linkedit_end_offset =
955                Some(format_integer(sig.linkedit_segment_end_offset));
956            entity.macho_signature_start_offset =
957                Some(format_integer(sig.signature_file_start_offset));
958            entity.linkedit_signature_start_offset =
959                Some(format_integer(sig.signature_segment_start_offset));
960        }
961
962        if let Some(sig) = macho.code_signature()? {
963            if let Some(sig_info) = macho.find_signature_data()? {
964                entity.macho_signature_end_offset = Some(format_integer(
965                    sig_info.signature_file_start_offset + sig.length as usize,
966                ));
967                entity.linkedit_signature_end_offset = Some(format_integer(
968                    sig_info.signature_segment_start_offset + sig.length as usize,
969                ));
970
971                let mut linkedit_remaining =
972                    sig_info.linkedit_segment_end_offset - sig_info.linkedit_segment_start_offset;
973                linkedit_remaining -= sig_info.signature_segment_start_offset;
974                linkedit_remaining -= sig.length as usize;
975                entity.linkedit_bytes_after_signature = Some(format_integer(linkedit_remaining));
976            }
977
978            entity.signature = Some(sig.try_into()?);
979        }
980
981        Ok(entity)
982    }
983
984    fn resolve_bundle_entities(
985        bundle: &DirectoryBundle,
986    ) -> Result<Vec<FileEntity>, AppleCodesignError> {
987        let mut entities = vec![];
988
989        for file in bundle
990            .files(true)
991            .map_err(AppleCodesignError::DirectoryBundle)?
992        {
993            entities.extend(Self::resolve_bundle_file_entity(
994                bundle.root_dir().to_path_buf(),
995                file,
996            )?);
997        }
998
999        Ok(entities)
1000    }
1001
1002    fn resolve_bundle_file_entity(
1003        base_path: PathBuf,
1004        file: DirectoryBundleFile,
1005    ) -> Result<Vec<FileEntity>, AppleCodesignError> {
1006        let main_relative_path = match file.absolute_path().strip_prefix(&base_path) {
1007            Ok(path) => path.to_path_buf(),
1008            Err(_) => file.absolute_path().to_path_buf(),
1009        };
1010
1011        let mut entities = vec![];
1012
1013        let mut default_entity =
1014            FileEntity::from_path(file.absolute_path(), Some(&main_relative_path))?;
1015
1016        let file_name = file
1017            .absolute_path()
1018            .file_name()
1019            .expect("path should have file name")
1020            .to_string_lossy();
1021        let parent_dir = file
1022            .absolute_path()
1023            .parent()
1024            .expect("path should have parent directory");
1025
1026        // There may be bugs in the code identifying the role of files in bundles.
1027        // So rely on our own heuristics to detect and report on the file type.
1028        if default_entity.symlink_target.is_some() {
1029            entities.push(default_entity);
1030        } else if parent_dir.ends_with("_CodeSignature") {
1031            if file_name == "CodeResources" {
1032                let data = isideload_vfs::fs::read(file.absolute_path())?;
1033
1034                default_entity.entity =
1035                    SignatureEntity::BundleCodeSignatureFile(CodeSignatureFile::ResourcesXml(
1036                        String::from_utf8_lossy(&data)
1037                            .split('\n')
1038                            .map(|x| x.replace('\t', "  "))
1039                            .collect::<Vec<_>>(),
1040                    ));
1041
1042                entities.push(default_entity);
1043            } else {
1044                default_entity.entity =
1045                    SignatureEntity::BundleCodeSignatureFile(CodeSignatureFile::Other);
1046
1047                entities.push(default_entity);
1048            }
1049        } else if file_name == "CodeResources" {
1050            default_entity.entity =
1051                SignatureEntity::BundleCodeSignatureFile(CodeSignatureFile::NotarizationTicket);
1052
1053            entities.push(default_entity);
1054        } else {
1055            let data = isideload_vfs::fs::read(file.absolute_path())?;
1056
1057            match Self::resolve_macho_entities_from_data(
1058                file.absolute_path(),
1059                &data,
1060                Some(&main_relative_path),
1061            ) {
1062                Ok(extra) => {
1063                    entities.extend(extra);
1064                }
1065                Err(_) => {
1066                    // Just some extra file.
1067                    entities.push(default_entity);
1068                }
1069            }
1070        }
1071
1072        Ok(entities)
1073    }
1074
1075    fn resolve_flat_package_entities(path: &Path) -> Result<Vec<FileEntity>, AppleCodesignError> {
1076        let mut xar = XarReader::new(File::open(path)?)?;
1077
1078        let default_entity = FileEntity::from_path(path, None)?;
1079
1080        let mut entities = vec![];
1081
1082        let mut entity = default_entity.clone();
1083        entity.sub_path = Some("toc".to_string());
1084        entity.entity =
1085            SignatureEntity::XarTableOfContents(XarTableOfContents::from_xar(&mut xar)?);
1086        entities.push(entity);
1087
1088        // Now emit entries for all files in table of contents.
1089        for (name, file) in xar.files()? {
1090            let mut entity = default_entity.clone();
1091            entity.sub_path = Some(name);
1092            entity.entity = SignatureEntity::XarMember(XarFile::try_from(&file)?);
1093            entities.push(entity);
1094        }
1095
1096        Ok(entities)
1097    }
1098}