apple_codesign/cli/
debug_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_requirement::CodeRequirements,
9        cryptography::DigestType,
10        error::{AppleCodesignError, Result},
11    },
12    clap::{Parser, ValueEnum},
13    log::warn,
14    std::{ops::Deref, path::PathBuf},
15};
16
17#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
18pub enum MachOArch {
19    Aarch64,
20    X86_64,
21}
22
23#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
24pub enum MachOFileType {
25    Executable,
26    Dylib,
27}
28
29impl MachOFileType {
30    fn to_header_filetype(&self) -> u32 {
31        match self {
32            Self::Executable => object::macho::MH_EXECUTE,
33            Self::Dylib => object::macho::MH_DYLIB,
34        }
35    }
36}
37
38#[derive(Parser)]
39pub struct DebugCreateCodeRequirements {
40    /// Code requirement expression to emit.
41    #[arg(long, value_enum)]
42    code_requirement: crate::policy::ExecutionPolicy,
43
44    /// Path to write binary requirements to.
45    path: PathBuf,
46}
47
48impl CliCommand for DebugCreateCodeRequirements {
49    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
50        let expression = self.code_requirement.deref();
51
52        let mut reqs = CodeRequirements::default();
53        reqs.push(expression.clone());
54
55        let data = reqs.to_blob_data()?;
56
57        println!("writing code requirements to {}", self.path.display());
58
59        if let Some(parent) = self.path.parent() {
60            std::fs::create_dir_all(parent)?;
61        }
62
63        std::fs::write(&self.path, data)?;
64
65        Ok(())
66    }
67}
68
69#[derive(Parser)]
70pub struct DebugCreateConstraints {
71    /// Team identifier constraint.
72    #[arg(long)]
73    team_id: Option<String>,
74
75    /// Path to write plist XML to.
76    path: PathBuf,
77}
78
79impl CliCommand for DebugCreateConstraints {
80    fn run(&self, _context: &Context) -> Result<()> {
81        let mut v = plist::Dictionary::default();
82
83        if let Some(id) = &self.team_id {
84            v.insert("team-identifier".into(), id.to_string().into());
85        }
86
87        let mut reqs = plist::Dictionary::default();
88
89        reqs.insert("$or".into(), v.into());
90
91        let v = plist::Value::Dictionary(reqs);
92
93        println!("writing constraints plist to {}", self.path.display());
94        v.to_file_xml(&self.path)?;
95
96        Ok(())
97    }
98}
99
100#[derive(Parser)]
101pub struct DebugCreateEntitlements {
102    /// Add the `get-task-allow` entitlement.
103    #[arg(long)]
104    get_task_allow: bool,
105
106    /// Add the `run-unsigned-code` entitlement.
107    #[arg(long)]
108    run_unsigned_code: bool,
109
110    /// Add the `com.apple.private.cs.debugger` entitlement.
111    #[arg(long)]
112    debugger: bool,
113
114    /// Add the `dynamic-codesigning` entitlement.
115    #[arg(long)]
116    dynamic_code_signing: bool,
117
118    /// Add the `com.apple.private.skip-library-validation` entitlement.
119    #[arg(long)]
120    skip_library_validation: bool,
121
122    /// Add the `com.apple.private.amfi.can-load-cdhash` entitlement.
123    #[arg(long)]
124    can_load_cd_hash: bool,
125
126    /// Add the `com.apple.private.amfi.can-execute-cdhash` entitlement.
127    #[arg(long)]
128    can_execute_cd_hash: bool,
129
130    /// Path to write entitlements to.
131    output_path: PathBuf,
132}
133
134impl CliCommand for DebugCreateEntitlements {
135    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
136        let mut d = plist::Dictionary::default();
137
138        if self.get_task_allow {
139            d.insert("get-task-allow".into(), true.into());
140        }
141        if self.run_unsigned_code {
142            d.insert("run-unsigned-code".into(), true.into());
143        }
144        if self.debugger {
145            d.insert("com.apple.private.cs.debugger".into(), true.into());
146        }
147        if self.dynamic_code_signing {
148            d.insert("dynamic-codesigning".into(), true.into());
149        }
150        if self.skip_library_validation {
151            d.insert(
152                "com.apple.private.skip-library-validation".into(),
153                true.into(),
154            );
155        }
156        if self.can_load_cd_hash {
157            d.insert("com.apple.private.amfi.can-load-cdhash".into(), true.into());
158        }
159        if self.can_execute_cd_hash {
160            d.insert(
161                "com.apple.private.amfi.can-execute-cdhash".into(),
162                true.into(),
163            );
164        }
165
166        let value = plist::Value::from(d);
167        let mut xml = vec![];
168        value.to_writer_xml(&mut xml)?;
169
170        warn!("writing {}", self.output_path.display());
171        if let Some(parent) = self.output_path.parent() {
172            std::fs::create_dir_all(parent)?;
173        }
174
175        std::fs::write(&self.output_path, &xml)?;
176
177        Ok(())
178    }
179}
180
181#[derive(Parser)]
182pub struct DebugCreateInfoPlist {
183    /// Name of the bundle.
184    #[arg(long)]
185    bundle_name: String,
186
187    /// Bundle package type.
188    #[arg(long, default_value = "APPL")]
189    package_type: String,
190
191    /// CFBundleExecutable value.
192    #[arg(long)]
193    bundle_executable: Option<String>,
194
195    /// Bundle identifier.
196    #[arg(long, default_value = "com.example.mybundle")]
197    bundle_identifier: String,
198
199    /// Bundle version.
200    #[arg(long, default_value = "1.0.0")]
201    bundle_version: String,
202
203    /// Path to write Info.plist to.
204    output_path: PathBuf,
205
206    /// Write an empty Info.plist file. Other arguments ignored.
207    #[arg(long)]
208    empty: bool,
209}
210
211impl CliCommand for DebugCreateInfoPlist {
212    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
213        let mut d = plist::Dictionary::default();
214
215        if !self.empty {
216            d.insert("CFBundleName".into(), self.bundle_name.clone().into());
217            d.insert(
218                "CFBundlePackageType".into(),
219                self.package_type.clone().into(),
220            );
221            d.insert(
222                "CFBundleDisplayName".into(),
223                self.bundle_name.clone().into(),
224            );
225            if let Some(exe) = &self.bundle_executable {
226                d.insert("CFBundleExecutable".into(), exe.clone().into());
227            }
228            d.insert(
229                "CFBundleIdentifier".into(),
230                self.bundle_identifier.clone().into(),
231            );
232            d.insert("CFBundleVersion".into(), self.bundle_version.clone().into());
233            d.insert("CFBundleSignature".into(), "sig".into());
234            d.insert("CFBundleExecutable".into(), self.bundle_name.clone().into());
235        }
236
237        let value = plist::Value::from(d);
238
239        let mut xml = vec![];
240        value.to_writer_xml(&mut xml)?;
241
242        println!("writing {}", self.output_path.display());
243        if let Some(parent) = self.output_path.parent() {
244            std::fs::create_dir_all(parent)?;
245        }
246
247        std::fs::write(&self.output_path, &xml)?;
248
249        Ok(())
250    }
251}
252
253#[derive(Parser)]
254pub struct DebugCreateMachO {
255    /// Architecture of Mach-O binary.
256    #[arg(long, value_enum, default_value_t = MachOArch::Aarch64)]
257    architecture: MachOArch,
258
259    /// The Mach-O file type.
260    #[arg(long, value_enum, default_value_t = MachOFileType::Executable)]
261    file_type: MachOFileType,
262
263    /// Do not write platform targeting to Mach-O binary.
264    #[arg(long)]
265    no_targeting: bool,
266
267    /// The minimum operating system version the binary will run on.
268    #[arg(long)]
269    minimum_os_version: Option<semver::Version>,
270
271    /// The platform SDK version used to build the binary.
272    #[arg(long)]
273    sdk_version: Option<semver::Version>,
274
275    /// Set the file start offset of the __TEXT segment.
276    #[arg(long)]
277    text_segment_start_offset: Option<usize>,
278
279    /// Filename of Mach-O binary to write.
280    output_path: PathBuf,
281}
282
283impl CliCommand for DebugCreateMachO {
284    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
285        let mut builder = match self.architecture {
286            MachOArch::Aarch64 => {
287                crate::macho_builder::MachOBuilder::new_aarch64(self.file_type.to_header_filetype())
288            }
289            MachOArch::X86_64 => {
290                crate::macho_builder::MachOBuilder::new_x86_64(self.file_type.to_header_filetype())
291            }
292        };
293
294        let target = match (
295            self.no_targeting,
296            &self.minimum_os_version,
297            &self.sdk_version,
298        ) {
299            (true, _, _) => None,
300            (false, None, None) => {
301                warn!("assuming default minimum version 11.0.0");
302
303                Some(crate::macho::MachoTarget {
304                    platform: crate::Platform::MacOs,
305                    minimum_os_version: semver::Version::new(11, 0, 0),
306                    sdk_version: semver::Version::new(11, 0, 0),
307                })
308            }
309            (false, _, _) => {
310                let minimum_os_version = self
311                    .minimum_os_version
312                    .clone()
313                    .unwrap_or_else(|| self.sdk_version.clone().unwrap());
314                let sdk_version = self
315                    .sdk_version
316                    .clone()
317                    .unwrap_or_else(|| self.minimum_os_version.clone().unwrap());
318
319                Some(crate::macho::MachoTarget {
320                    platform: crate::Platform::MacOs,
321                    minimum_os_version,
322                    sdk_version,
323                })
324            }
325        };
326
327        if let Some(target) = target {
328            builder = builder.macho_target(target);
329        }
330
331        if let Some(offset) = self.text_segment_start_offset {
332            builder = builder.text_segment_start_offset(offset);
333        }
334
335        let data = builder.write_macho()?;
336
337        warn!("writing Mach-O to {}", self.output_path.display());
338        if let Some(parent) = self.output_path.parent() {
339            std::fs::create_dir_all(parent)?;
340        }
341
342        std::fs::write(&self.output_path, data)?;
343
344        Ok(())
345    }
346}
347
348#[derive(Parser)]
349pub struct DebugFileTree {
350    /// Directory to walk.
351    path: PathBuf,
352}
353
354impl CliCommand for DebugFileTree {
355    fn run(&self, _context: &Context) -> Result<(), AppleCodesignError> {
356        let root = self
357            .path
358            .components()
359            .last()
360            .expect("should have final component")
361            .as_os_str()
362            .to_string_lossy()
363            .to_string();
364
365        for entry in walkdir::WalkDir::new(&self.path).sort_by_file_name() {
366            let entry = entry?;
367
368            let path = entry.path();
369
370            let rel_path = if let Ok(p) = path.strip_prefix(&self.path) {
371                format!("{}/{}", root, p.to_string_lossy().replace('\\', "/"))
372            } else {
373                root.clone()
374            };
375
376            let metadata = entry.metadata()?;
377
378            let entry_type = if metadata.is_symlink() {
379                'l'
380            } else if metadata.is_dir() {
381                'd'
382            } else if metadata.is_file() {
383                'f'
384            } else {
385                'u'
386            };
387
388            let sha256 = if entry_type == 'f' {
389                let data = std::fs::read(path)?;
390                hex::encode(DigestType::Sha256.digest_data(&data)?)[0..20].to_string()
391            } else {
392                " ".repeat(20)
393            };
394
395            let link_target = if entry_type == 'l' {
396                format!(" -> {}", std::fs::read_link(path)?.to_string_lossy())
397            } else {
398                "".to_string()
399            };
400
401            println!("{} {} {}{}", entry_type, sha256, rel_path, link_target);
402        }
403
404        Ok(())
405    }
406}