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