1use {
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
66pub 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
80pub 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
94pub fn path_is_macho(path: impl AsRef<Path>) -> Result<bool, AppleCodesignError> {
96 Ok(MachOType::from_path(path)?.is_some())
97}
98
99#[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 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
169fn 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 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 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#[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#[derive(Clone, Debug, Serialize)]
468pub struct CodeSignature {
469 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 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 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
858pub enum SignatureReader {
860 Dmg(PathBuf, Box<DmgReader>),
861 MachO(PathBuf, Vec<u8>),
862 Bundle(Box<DirectoryBundle>),
863 FlatPackage(PathBuf),
864}
865
866impl SignatureReader {
867 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 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 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 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 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}