apple_codesign/cli/
extract_commands.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
5use {
6    crate::{
7        cli::{CliCommand, Context},
8        code_directory::CodeDirectoryBlob,
9        cryptography::DigestType,
10        embedded_signature::{Blob, CodeSigningSlot, RequirementSetBlob},
11        error::AppleCodesignError,
12        macho::MachFile,
13    },
14    base64::{engine::general_purpose::STANDARD as STANDARD_ENGINE, Engine},
15    clap::{Parser, Subcommand},
16    cryptographic_message_syntax::SignedData,
17    std::{io::Write, path::PathBuf},
18};
19
20fn print_signed_data(
21    prefix: &str,
22    signed_data: &SignedData,
23    external_content: Option<Vec<u8>>,
24) -> Result<(), AppleCodesignError> {
25    println!(
26        "{}signed content (embedded): {:?}",
27        prefix,
28        signed_data.signed_content().map(hex::encode)
29    );
30    println!(
31        "{}signed content (external): {:?}... ({} bytes)",
32        prefix,
33        external_content.as_ref().map(|x| hex::encode(&x[0..40])),
34        external_content.as_ref().map(|x| x.len()).unwrap_or(0),
35    );
36
37    let content = if let Some(v) = signed_data.signed_content() {
38        Some(v)
39    } else {
40        external_content.as_ref().map(|v| v.as_ref())
41    };
42
43    if let Some(content) = content {
44        println!(
45            "{}signed content SHA-1:   {}",
46            prefix,
47            hex::encode(DigestType::Sha1.digest_data(content)?)
48        );
49        println!(
50            "{}signed content SHA-256: {}",
51            prefix,
52            hex::encode(DigestType::Sha256.digest_data(content)?)
53        );
54        println!(
55            "{}signed content SHA-384: {}",
56            prefix,
57            hex::encode(DigestType::Sha384.digest_data(content)?)
58        );
59        println!(
60            "{}signed content SHA-512: {}",
61            prefix,
62            hex::encode(DigestType::Sha512.digest_data(content)?)
63        );
64    }
65    println!(
66        "{}certificate count: {}",
67        prefix,
68        signed_data.certificates().count()
69    );
70    for (i, cert) in signed_data.certificates().enumerate() {
71        println!(
72            "{}certificate #{}: subject CN={}; self signed={}",
73            prefix,
74            i,
75            cert.subject_common_name()
76                .unwrap_or_else(|| "<unknown>".to_string()),
77            cert.subject_is_issuer()
78        );
79    }
80    println!("{}signer count: {}", prefix, signed_data.signers().count());
81    for (i, signer) in signed_data.signers().enumerate() {
82        println!(
83            "{}signer #{}: digest algorithm: {:?}",
84            prefix,
85            i,
86            signer.digest_algorithm()
87        );
88        println!(
89            "{}signer #{}: signature algorithm: {:?}",
90            prefix,
91            i,
92            signer.signature_algorithm()
93        );
94
95        if let Some(sa) = signer.signed_attributes() {
96            println!(
97                "{}signer #{}: content type: {}",
98                prefix,
99                i,
100                sa.content_type()
101            );
102            println!(
103                "{}signer #{}: message digest: {}",
104                prefix,
105                i,
106                hex::encode(sa.message_digest())
107            );
108            println!(
109                "{}signer #{}: signing time: {:?}",
110                prefix,
111                i,
112                sa.signing_time()
113            );
114        }
115
116        let digested_data = signer.signed_content_with_signed_data(signed_data);
117
118        println!(
119            "{}signer #{}: signature content SHA-1:   {}",
120            prefix,
121            i,
122            hex::encode(DigestType::Sha1.digest_data(&digested_data)?)
123        );
124        println!(
125            "{}signer #{}: signature content SHA-256: {}",
126            prefix,
127            i,
128            hex::encode(DigestType::Sha256.digest_data(&digested_data)?)
129        );
130        println!(
131            "{}signer #{}: signature content SHA-384: {}",
132            prefix,
133            i,
134            hex::encode(DigestType::Sha384.digest_data(&digested_data)?)
135        );
136        println!(
137            "{}signer #{}: signature content SHA-512: {}",
138            prefix,
139            i,
140            hex::encode(DigestType::Sha512.digest_data(&digested_data)?)
141        );
142
143        if signed_data.signed_content().is_some() {
144            println!(
145                "{}signer #{}: digest valid: {}",
146                prefix,
147                i,
148                signer
149                    .verify_message_digest_with_signed_data(signed_data)
150                    .is_ok()
151            );
152        }
153        println!(
154            "{}signer #{}: signature valid: {}",
155            prefix,
156            i,
157            signer
158                .verify_signature_with_signed_data(signed_data)
159                .is_ok()
160        );
161
162        println!(
163            "{}signer #{}: time-stamp token present: {}",
164            prefix,
165            i,
166            signer.time_stamp_token_signed_data()?.is_some()
167        );
168
169        if let Some(tsp_signed_data) = signer.time_stamp_token_signed_data()? {
170            let prefix = format!("{prefix}signer #{i}: time-stamp token: ");
171
172            print_signed_data(&prefix, &tsp_signed_data, None)?;
173        }
174    }
175
176    Ok(())
177}
178
179#[derive(Clone, Parser)]
180struct ExtractCommon {
181    /// Path to Mach-O binary to examine
182    path: PathBuf,
183}
184
185#[derive(Clone, Subcommand)]
186enum ExtractData {
187    /// Code directory blobs.
188    Blobs(ExtractCommon),
189    /// Information about cryptographic message syntax signature.
190    CmsInfo(ExtractCommon),
191    /// PEM encoded cryptographic message syntax signature.
192    CmsPem(ExtractCommon),
193    /// Binary cryptographic message syntax signature. Should be BER encoded ASN.1 data.
194    CmsRaw(ExtractCommon),
195    /// ASN.1 decoded cryptographic message syntax data.
196    Cms(ExtractCommon),
197    /// Information from the main code directory data structure.
198    CodeDirectory(ExtractCommon),
199    /// Raw binary data composing the code directory data structure.
200    CodeDirectoryRaw(ExtractCommon),
201    /// Reserialize the parsed code directory, parse it again, and then print it like `code-directory` would.
202    CodeDirectorySerialized(ExtractCommon),
203    /// Reserialize the parsed code directory and emit its binary.
204    ///
205    /// Useful for comparing round-tripping of code directory data.
206    CodeDirectorySerializedRaw(ExtractCommon),
207    /// Information about the __LINKEDIT Mach-O segment.
208    LinkeditInfo(ExtractCommon),
209    /// Complete content of the __LINKEDIT Mach-O segment.
210    LinkeditSegmentRaw(ExtractCommon),
211    /// Mach-O file header data.
212    MachoHeader(ExtractCommon),
213    /// High-level information about Mach-O load commands.
214    MachoLoadCommands(ExtractCommon),
215    /// Debug formatted Mach-O load command data structures.
216    MachoLoadCommandsRaw(ExtractCommon),
217    /// Information about Mach-O segments.
218    MachoSegments(ExtractCommon),
219    /// Mach-O targeting info.
220    MachoTarget(ExtractCommon),
221    /// Parsed code requirement statement/expression.
222    Requirements(ExtractCommon),
223    /// Raw binary data composing the requirements blob/slot.
224    RequirementsRaw(ExtractCommon),
225    /// Dump the internal Rust data structures representing the requirements expressions.
226    RequirementsRust(ExtractCommon),
227    /// Reserialize the code requirements blob, parse it again, and then print it like `requirements` would.
228    RequirementsSerialized(ExtractCommon),
229    /// Like `requirements-serialized` except emit the binary data representation.
230    RequirementsSerializedRaw(ExtractCommon),
231    /// Raw binary data constituting the signature data embedded in the binary.
232    SignatureRaw(ExtractCommon),
233    /// Show information about the SuperBlob record and high-level details of embedded Blob records.
234    Superblob(ExtractCommon),
235}
236
237impl ExtractData {
238    fn common_args(&self) -> &ExtractCommon {
239        match self {
240            ExtractData::Blobs(x) => x,
241            ExtractData::CmsInfo(x) => x,
242            ExtractData::CmsPem(x) => x,
243            ExtractData::CmsRaw(x) => x,
244            ExtractData::Cms(x) => x,
245            ExtractData::CodeDirectoryRaw(x) => x,
246            ExtractData::CodeDirectorySerializedRaw(x) => x,
247            ExtractData::CodeDirectorySerialized(x) => x,
248            ExtractData::CodeDirectory(x) => x,
249            ExtractData::LinkeditInfo(x) => x,
250            ExtractData::LinkeditSegmentRaw(x) => x,
251            ExtractData::MachoHeader(x) => x,
252            ExtractData::MachoLoadCommands(x) => x,
253            ExtractData::MachoLoadCommandsRaw(x) => x,
254            ExtractData::MachoSegments(x) => x,
255            ExtractData::MachoTarget(x) => x,
256            ExtractData::RequirementsRaw(x) => x,
257            ExtractData::RequirementsRust(x) => x,
258            ExtractData::RequirementsSerializedRaw(x) => x,
259            ExtractData::RequirementsSerialized(x) => x,
260            ExtractData::Requirements(x) => x,
261            ExtractData::SignatureRaw(x) => x,
262            ExtractData::Superblob(x) => x,
263        }
264    }
265}
266
267#[derive(Parser)]
268pub struct Extract {
269    /// Index of Mach-O binary to operate on within a universal/fat binary
270    #[arg(long, global = true, default_value = "0")]
271    universal_index: usize,
272
273    /// Which data to extract and how to format it
274    #[command(subcommand)]
275    data: ExtractData,
276}
277
278impl CliCommand for Extract {
279    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
280        let common = self.data.common_args();
281
282        let data = std::fs::read(&common.path)?;
283        let mach = MachFile::parse(&data)?;
284        let macho = mach.nth_macho(self.universal_index)?;
285
286        match self.data {
287            ExtractData::Blobs(_) => {
288                let embedded = macho
289                    .code_signature()?
290                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
291
292                for blob in embedded.blobs {
293                    let parsed = blob.into_parsed_blob()?;
294                    println!("{parsed:#?}");
295                }
296            }
297            ExtractData::CmsInfo(_) => {
298                let embedded = macho
299                    .code_signature()?
300                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
301
302                if let Some(cms) = embedded.signature_data()? {
303                    let signed_data = SignedData::parse_ber(cms)?;
304
305                    let cd_data = if let Ok(Some(blob)) = embedded.code_directory() {
306                        Some(blob.to_blob_bytes()?)
307                    } else {
308                        None
309                    };
310
311                    print_signed_data("", &signed_data, cd_data)?;
312                } else {
313                    eprintln!("no CMS data");
314                }
315            }
316            ExtractData::CmsPem(_) => {
317                let embedded = macho
318                    .code_signature()?
319                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
320
321                if let Some(cms) = embedded.signature_data()? {
322                    print!("{}", pem::encode(&pem::Pem::new("PKCS7", cms.to_vec())));
323                } else {
324                    eprintln!("no CMS data");
325                }
326            }
327            ExtractData::CmsRaw(_) => {
328                let embedded = macho
329                    .code_signature()?
330                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
331
332                if let Some(cms) = embedded.signature_data()? {
333                    std::io::stdout().write_all(cms)?;
334                } else {
335                    eprintln!("no CMS data");
336                }
337            }
338            ExtractData::Cms(_) => {
339                let embedded = macho
340                    .code_signature()?
341                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
342
343                if let Some(signed_data) = embedded.signed_data()? {
344                    println!("{signed_data:#?}");
345                } else {
346                    eprintln!("no CMS data");
347                }
348            }
349            ExtractData::CodeDirectoryRaw(_) => {
350                let embedded = macho
351                    .code_signature()?
352                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
353
354                if let Some(blob) = embedded.find_slot(CodeSigningSlot::CodeDirectory) {
355                    std::io::stdout().write_all(blob.data)?;
356                } else {
357                    eprintln!("no code directory");
358                }
359            }
360            ExtractData::CodeDirectorySerializedRaw(_) => {
361                let embedded = macho
362                    .code_signature()?
363                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
364
365                if let Ok(Some(cd)) = embedded.code_directory() {
366                    std::io::stdout().write_all(&cd.to_blob_bytes()?)?;
367                } else {
368                    eprintln!("no code directory");
369                }
370            }
371            ExtractData::CodeDirectorySerialized(_) => {
372                let embedded = macho
373                    .code_signature()?
374                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
375
376                if let Ok(Some(cd)) = embedded.code_directory() {
377                    let serialized = cd.to_blob_bytes()?;
378                    println!("{:#?}", CodeDirectoryBlob::from_blob_bytes(&serialized)?);
379                }
380            }
381            ExtractData::CodeDirectory(_) => {
382                let embedded = macho
383                    .code_signature()?
384                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
385
386                if let Some(cd) = embedded.code_directory()? {
387                    println!("{cd:#?}");
388                } else {
389                    eprintln!("no code directory");
390                }
391            }
392            ExtractData::LinkeditInfo(_) => {
393                let sig = macho
394                    .find_signature_data()?
395                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
396                println!("__LINKEDIT segment index: {}", sig.linkedit_segment_index);
397                println!(
398                    "__LINKEDIT segment start offset: {}",
399                    sig.linkedit_segment_start_offset
400                );
401                println!(
402                    "__LINKEDIT segment end offset: {}",
403                    sig.linkedit_segment_end_offset
404                );
405                println!(
406                    "__LINKEDIT segment size: {}",
407                    sig.linkedit_segment_data.len()
408                );
409                println!(
410                    "__LINKEDIT signature global start offset: {}",
411                    sig.signature_file_start_offset
412                );
413                println!(
414                    "__LINKEDIT signature global end offset: {}",
415                    sig.signature_file_end_offset
416                );
417                println!(
418                    "__LINKEDIT signature local segment start offset: {}",
419                    sig.signature_segment_start_offset
420                );
421                println!(
422                    "__LINKEDIT signature local segment end offset: {}",
423                    sig.signature_segment_end_offset
424                );
425                println!("__LINKEDIT signature size: {}", sig.signature_data.len());
426            }
427            ExtractData::LinkeditSegmentRaw(_) => {
428                let sig = macho
429                    .find_signature_data()?
430                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
431                std::io::stdout().write_all(sig.linkedit_segment_data)?;
432            }
433            ExtractData::MachoHeader(_) => {
434                println!("{:#?}", macho.macho.header);
435            }
436            ExtractData::MachoLoadCommands(_) => {
437                println!("load command count: {}", macho.macho.load_commands.len());
438
439                for command in &macho.macho.load_commands {
440                    println!(
441                        "{}; offsets=0x{:x}-0x{:x} ({}-{}); size={}",
442                        goblin::mach::load_command::cmd_to_str(command.command.cmd()),
443                        command.offset,
444                        command.offset + command.command.cmdsize(),
445                        command.offset,
446                        command.offset + command.command.cmdsize(),
447                        command.command.cmdsize(),
448                    );
449                }
450            }
451            ExtractData::MachoLoadCommandsRaw(_) => {
452                for command in &macho.macho.load_commands {
453                    println!("{:?}", command);
454                }
455            }
456            ExtractData::MachoSegments(_) => {
457                println!("segments count: {}", macho.macho.segments.len());
458                for (segment_index, segment) in macho.macho.segments.iter().enumerate() {
459                    let sections = segment.sections()?;
460
461                    println!(
462                        "segment #{}; {}; offsets=0x{:x}-0x{:x} ({}-{}); addresses=0x{:x}-0x{:x}; vm/file size {}/{}; section count {}",
463                        segment_index,
464                        segment.name()?,
465                        segment.fileoff,
466                        segment.fileoff as usize + segment.data.len(),
467                        segment.fileoff,
468                        segment.fileoff as usize + segment.data.len(),
469                        segment.vmaddr,
470                        segment.vmaddr + segment.vmsize,
471                        segment.vmsize,
472                        segment.filesize,
473                        sections.len()
474                    );
475                    for (section_index, (section, _)) in sections.into_iter().enumerate() {
476                        println!(
477                            "segment #{}; section #{}: {}; offsets=0x{:x}-0x{:x} ({}-{}); addresses=0x{:x}-0x{:x}; size {}; align={}; flags={}",
478                            segment_index,
479                            section_index,
480                            section.name()?,
481                            section.offset,
482                            section.offset as u64 + section.size,
483                            section.offset,
484                            section.offset as u64 + section.size,
485                            section.addr,
486                            section.addr + section.size,
487                            section.size,
488                            section.align,
489                            section.flags,
490                        );
491                    }
492                }
493            }
494            ExtractData::MachoTarget(_) => {
495                if let Some(target) = macho.find_targeting()? {
496                    println!("Platform: {}", target.platform);
497                    println!("Minimum OS: {}", target.minimum_os_version);
498                    println!("SDK: {}", target.sdk_version);
499                } else {
500                    println!("Unable to resolve Mach-O targeting from load commands");
501                }
502            }
503            ExtractData::RequirementsRaw(_) => {
504                let embedded = macho
505                    .code_signature()?
506                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
507
508                if let Some(blob) = embedded.find_slot(CodeSigningSlot::RequirementSet) {
509                    std::io::stdout().write_all(blob.data)?;
510                } else {
511                    eprintln!("no requirements");
512                }
513            }
514            ExtractData::RequirementsRust(_) => {
515                let embedded = macho
516                    .code_signature()?
517                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
518
519                if let Some(reqs) = embedded.code_requirements()? {
520                    for (typ, req) in &reqs.requirements {
521                        for expr in req.parse_expressions()?.iter() {
522                            println!("{typ} => {expr:#?}");
523                        }
524                    }
525                } else {
526                    eprintln!("no requirements");
527                }
528            }
529            ExtractData::RequirementsSerializedRaw(_) => {
530                let embedded = macho
531                    .code_signature()?
532                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
533
534                if let Some(reqs) = embedded.code_requirements()? {
535                    std::io::stdout().write_all(&reqs.to_blob_bytes()?)?;
536                } else {
537                    eprintln!("no requirements");
538                }
539            }
540            ExtractData::RequirementsSerialized(_) => {
541                let embedded = macho
542                    .code_signature()?
543                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
544
545                if let Some(reqs) = embedded.code_requirements()? {
546                    let serialized = reqs.to_blob_bytes()?;
547                    println!("{:#?}", RequirementSetBlob::from_blob_bytes(&serialized)?);
548                } else {
549                    eprintln!("no requirements");
550                }
551            }
552            ExtractData::Requirements(_) => {
553                let embedded = macho
554                    .code_signature()?
555                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
556
557                if let Some(reqs) = embedded.code_requirements()? {
558                    for (typ, req) in &reqs.requirements {
559                        for expr in req.parse_expressions()?.iter() {
560                            println!("{typ} => {expr}");
561                        }
562                    }
563                } else {
564                    eprintln!("no requirements");
565                }
566            }
567            ExtractData::SignatureRaw(_) => {
568                let sig = macho
569                    .find_signature_data()?
570                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
571                std::io::stdout().write_all(sig.signature_data)?;
572            }
573            ExtractData::Superblob(_) => {
574                let sig = macho
575                    .find_signature_data()?
576                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
577                let embedded = macho
578                    .code_signature()?
579                    .ok_or(AppleCodesignError::BinaryNoCodeSignature)?;
580
581                println!("file start offset: {}", sig.signature_file_start_offset);
582                println!("file end offset: {}", sig.signature_file_end_offset);
583                println!(
584                    "__LINKEDIT start offset: {}",
585                    sig.signature_segment_start_offset
586                );
587                println!(
588                    "__LINKEDIT end offset: {}",
589                    sig.signature_segment_end_offset
590                );
591                println!("length: {}", embedded.length);
592                println!("blob count: {}", embedded.count);
593                println!("blobs:");
594                for blob in embedded.blobs {
595                    println!("- index: {}", blob.index);
596                    println!(
597                        "  offsets: 0x{:x}-0x{:x} ({}-{})",
598                        blob.offset,
599                        blob.offset + blob.length - 1,
600                        blob.offset,
601                        blob.offset + blob.length - 1
602                    );
603                    println!("  length: {}", blob.length);
604                    println!("  slot: {:?}", blob.slot);
605                    println!("  magic: {:?} (0x{:x})", blob.magic, u32::from(blob.magic));
606                    println!(
607                        "  sha1: {}",
608                        hex::encode(blob.digest_with(DigestType::Sha1)?)
609                    );
610                    println!(
611                        "  sha256: {}",
612                        hex::encode(blob.digest_with(DigestType::Sha256)?)
613                    );
614                    println!(
615                        "  sha256-truncated: {}",
616                        hex::encode(blob.digest_with(DigestType::Sha256Truncated)?)
617                    );
618                    println!(
619                        "  sha384: {}",
620                        hex::encode(blob.digest_with(DigestType::Sha384)?),
621                    );
622                    println!(
623                        "  sha512: {}",
624                        hex::encode(blob.digest_with(DigestType::Sha512)?),
625                    );
626                    println!(
627                        "  sha1-base64: {}",
628                        STANDARD_ENGINE.encode(blob.digest_with(DigestType::Sha1)?)
629                    );
630                    println!(
631                        "  sha256-base64: {}",
632                        STANDARD_ENGINE.encode(blob.digest_with(DigestType::Sha256)?)
633                    );
634                    println!(
635                        "  sha256-truncated-base64: {}",
636                        STANDARD_ENGINE.encode(blob.digest_with(DigestType::Sha256Truncated)?)
637                    );
638                    println!(
639                        "  sha384-base64: {}",
640                        STANDARD_ENGINE.encode(blob.digest_with(DigestType::Sha384)?)
641                    );
642                    println!(
643                        "  sha512-base64: {}",
644                        STANDARD_ENGINE.encode(blob.digest_with(DigestType::Sha512)?)
645                    );
646                }
647            }
648        }
649
650        Ok(())
651    }
652}