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