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 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
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
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
168fn 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 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 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#[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#[derive(Clone, Debug, Serialize)]
467pub struct CodeSignature {
468 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 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 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
857pub enum SignatureReader {
859 Dmg(PathBuf, Box<DmgReader>),
860 MachO(PathBuf, Vec<u8>),
861 Bundle(Box<DirectoryBundle>),
862 FlatPackage(PathBuf),
863}
864
865impl SignatureReader {
866 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 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 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 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 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}