isideload_apple_codesign/
signing.rs1use {
8 crate::{
9 bundle_signing::BundleSigner,
10 dmg::DmgSigner,
11 error::AppleCodesignError,
12 macho_signing::{MachOSigner, write_macho_file},
13 reader::PathType,
14 signing_settings::{SettingsScope, SigningSettings},
15 },
16 apple_xar::{reader::XarReader, signing::XarSigner},
17 isideload_vfs::fs::File,
18 log::{info, warn},
19 std::path::Path,
20};
21
22pub struct UnifiedSigner<'key> {
24 settings: SigningSettings<'key>,
25}
26
27impl<'key> UnifiedSigner<'key> {
28 pub fn new(settings: SigningSettings<'key>) -> Self {
30 Self { settings }
31 }
32
33 pub fn sign_path(
35 &self,
36 input_path: impl AsRef<Path>,
37 output_path: impl AsRef<Path>,
38 ) -> Result<(), AppleCodesignError> {
39 let input_path = input_path.as_ref();
40
41 match PathType::from_path(input_path)? {
42 PathType::Bundle => self.sign_bundle(input_path, output_path),
43 PathType::Dmg => self.sign_dmg(input_path, output_path),
44 PathType::MachO => self.sign_macho(input_path, output_path),
45 PathType::Xar => self.sign_xar(input_path, output_path),
46 PathType::Zip | PathType::Other => Err(AppleCodesignError::UnrecognizedPathType),
47 }
48 }
49
50 pub fn sign_path_in_place(&self, path: impl AsRef<Path>) -> Result<(), AppleCodesignError> {
55 let path = path.as_ref();
56
57 self.sign_path(path, path)
58 }
59
60 pub fn sign_macho(
62 &self,
63 input_path: impl AsRef<Path>,
64 output_path: impl AsRef<Path>,
65 ) -> Result<(), AppleCodesignError> {
66 let input_path = input_path.as_ref();
67 let output_path = output_path.as_ref();
68
69 warn!("signing {} as a Mach-O binary", input_path.display());
70
71 #[cfg(unix)]
72 {
73 use isideload_vfs::fs::PermissionsExt;
74 let mut perms = isideload_vfs::fs::metadata(input_path)?.permissions();
75 perms.set_mode(0o755);
76 isideload_vfs::fs::set_permissions(input_path, perms)?;
77 }
78
79 let macho_data = isideload_vfs::fs::read(input_path)?;
80
81 let mut settings = self.settings.clone();
82
83 settings.import_settings_from_macho(&macho_data)?;
84
85 if settings.binary_identifier(SettingsScope::Main).is_none() {
86 let identifier = path_identifier(input_path)?;
87
88 warn!("setting binary identifier to {}", identifier);
89 settings.set_binary_identifier(SettingsScope::Main, identifier);
90 }
91
92 warn!("parsing Mach-O");
93 let signer = MachOSigner::new(&macho_data)?;
94
95 let mut macho_data = vec![];
96 signer.write_signed_binary(&settings, &mut macho_data)?;
97 warn!("writing Mach-O to {}", output_path.display());
98 write_macho_file(input_path, output_path, &macho_data)?;
99
100 Ok(())
101 }
102
103 pub fn sign_dmg(
105 &self,
106 input_path: impl AsRef<Path>,
107 output_path: impl AsRef<Path>,
108 ) -> Result<(), AppleCodesignError> {
109 let input_path = input_path.as_ref();
110 let output_path = output_path.as_ref();
111
112 warn!("signing {} as a DMG", input_path.display());
113
114 let mut settings = self.settings.clone();
117
118 if settings.binary_identifier(SettingsScope::Main).is_none() {
119 let file_name = input_path
120 .file_stem()
121 .ok_or_else(|| {
122 AppleCodesignError::CliGeneralError("unable to resolve file name of DMG".into())
123 })?
124 .to_string_lossy();
125
126 warn!(
127 "setting binary identifier to {} (derived from file name)",
128 file_name
129 );
130 settings.set_binary_identifier(SettingsScope::Main, file_name);
131 }
132
133 if input_path != output_path {
138 info!(
139 "copying {} to {} in preparation for signing",
140 input_path.display(),
141 output_path.display()
142 );
143 if let Some(parent) = output_path.parent() {
144 isideload_vfs::fs::create_dir_all(parent)?;
145 }
146
147 isideload_vfs::fs::copy(input_path, output_path)?;
148 }
149
150 let signer = DmgSigner::default();
151 let mut fh = isideload_vfs::fs::File::options()
152 .read(true)
153 .write(true)
154 .open(output_path)?;
155 signer.sign_file(&settings, &mut fh)?;
156
157 Ok(())
158 }
159
160 pub fn sign_bundle(
162 &self,
163 input_path: impl AsRef<Path>,
164 output_path: impl AsRef<Path>,
165 ) -> Result<(), AppleCodesignError> {
166 let input_path = input_path.as_ref();
167 warn!("signing bundle at {}", input_path.display());
168
169 let mut signer = BundleSigner::new_from_path(input_path)?;
170 signer.collect_nested_bundles()?;
171 signer.write_signed_bundle(output_path, &self.settings)?;
172
173 Ok(())
174 }
175
176 pub fn sign_xar(
177 &self,
178 input_path: impl AsRef<Path>,
179 output_path: impl AsRef<Path>,
180 ) -> Result<(), AppleCodesignError> {
181 let input_path = input_path.as_ref();
182 let output_path = output_path.as_ref();
183
184 let output_path_temp =
188 output_path.with_file_name(if let Some(file_name) = output_path.file_name() {
189 file_name.to_string_lossy().to_string() + ".tmp"
190 } else {
191 "xar.tmp".to_string()
192 });
193
194 warn!(
195 "signing XAR pkg installer at {} to {}",
196 input_path.display(),
197 output_path_temp.display()
198 );
199
200 let (signing_key, signing_cert) = self
201 .settings
202 .signing_key()
203 .ok_or(AppleCodesignError::XarNoAdhoc)?;
204
205 {
206 let reader = XarReader::new(File::open(input_path)?)?;
207 let mut signer = XarSigner::new(reader);
208
209 let mut fh = File::create(&output_path_temp)?;
210 signer.sign(
211 &mut fh,
212 signing_key,
213 signing_cert,
214 self.settings.certificate_chain().iter().cloned(),
215 )?;
216 }
217
218 if output_path.exists() {
219 warn!("removing existing {}", output_path.display());
220 isideload_vfs::fs::remove_file(output_path)?;
221 }
222
223 warn!(
224 "renaming {} -> {}",
225 output_path_temp.display(),
226 output_path.display()
227 );
228 isideload_vfs::fs::rename(&output_path_temp, output_path)?;
229
230 Ok(())
231 }
232}
233
234pub fn path_identifier(path: impl AsRef<Path>) -> Result<String, AppleCodesignError> {
235 let path = path.as_ref();
236
237 let file_name = path
239 .file_name()
240 .ok_or_else(|| {
241 AppleCodesignError::PathIdentifier(format!("path {} lacks a file name", path.display()))
242 })?
243 .to_string_lossy()
244 .to_string();
245
246 let id = if let Some((prefix, extension)) = file_name.rsplit_once('.') {
248 if extension.chars().all(|c| c.is_ascii_digit()) {
249 file_name.as_str()
250 } else {
251 prefix
252 }
253 } else {
254 file_name.as_str()
255 };
256
257 let is_digit_or_dot = |c: char| c == '.' || c.is_ascii_digit();
258
259 let id = match id.chars().next() {
262 Some(first) => {
263 if is_digit_or_dot(first) {
264 return Ok(id.to_string());
265 } else {
266 id
267 }
268 }
269 None => {
270 return Ok(id.to_string());
271 }
272 };
273
274 let prefix = id.trim_end_matches(is_digit_or_dot);
280 let stripped = &id[prefix.len()..];
281
282 if stripped.is_empty() {
283 Ok(id.to_string())
284 } else {
285 let (prefix, stripped) = if matches!(stripped.chars().next(), Some('.')) {
287 (&id[0..prefix.len() + 1], &stripped[1..])
288 } else {
289 (prefix, stripped)
290 };
291
292 let id = prefix
295 .chars()
296 .chain(stripped.chars().take_while(|c| c.is_ascii_digit()))
297 .collect::<String>();
298
299 Ok(id)
300 }
301}
302
303#[cfg(test)]
304mod test {
305 use super::*;
306 #[test]
307 fn path_identifier_normalization() {
308 assert_eq!(path_identifier("foo").unwrap(), "foo");
309 assert_eq!(path_identifier("foo.dylib").unwrap(), "foo");
310 assert_eq!(path_identifier("/etc/foo.dylib").unwrap(), "foo");
311 assert_eq!(path_identifier("/etc/foo").unwrap(), "foo");
312
313 assert_eq!(path_identifier(".foo").unwrap(), "");
315 assert_eq!(path_identifier("123").unwrap(), "123");
316 assert_eq!(path_identifier(".foo.dylib").unwrap(), ".foo");
317 assert_eq!(path_identifier("123.dylib").unwrap(), "123");
318 assert_eq!(path_identifier("123.42").unwrap(), "123.42");
319
320 assert_eq!(path_identifier("foo1").unwrap(), "foo1");
323 assert_eq!(path_identifier("foo1.dylib").unwrap(), "foo1");
324 assert_eq!(path_identifier("foo1.2.dylib").unwrap(), "foo1");
325 assert_eq!(path_identifier("foo1.2").unwrap(), "foo1");
326 assert_eq!(path_identifier("foo1.2.3.4.dylib").unwrap(), "foo1");
327 assert_eq!(path_identifier("foo.1").unwrap(), "foo.1");
328 assert_eq!(path_identifier("foo.1.2.3").unwrap(), "foo.1");
329 assert_eq!(path_identifier("foo.1.2.dylib").unwrap(), "foo.1");
330 assert_eq!(path_identifier("foo.1.dylib").unwrap(), "foo.1");
331 }
332}