1use {
8 crate::{
9 bundle_signing::BundleSigner,
10 dmg::DmgSigner,
11 error::AppleCodesignError,
12 macho_signing::{write_macho_file, MachOSigner},
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 let macho_data = std::fs::read(input_path)?;
70
71 let mut settings = self.settings.clone();
72
73 settings.import_settings_from_macho(&macho_data)?;
74
75 if settings.binary_identifier(SettingsScope::Main).is_none() {
76 let identifier = path_identifier(input_path)?;
77
78 warn!("setting binary identifier to {}", identifier);
79 settings.set_binary_identifier(SettingsScope::Main, identifier);
80 }
81
82 warn!("parsing Mach-O");
83 let signer = MachOSigner::new(&macho_data)?;
84
85 let mut macho_data = vec![];
86 signer.write_signed_binary(&settings, &mut macho_data)?;
87 warn!("writing Mach-O to {}", output_path.display());
88 write_macho_file(input_path, output_path, &macho_data)?;
89
90 Ok(())
91 }
92
93 pub fn sign_dmg(
95 &self,
96 input_path: impl AsRef<Path>,
97 output_path: impl AsRef<Path>,
98 ) -> Result<(), AppleCodesignError> {
99 let input_path = input_path.as_ref();
100 let output_path = output_path.as_ref();
101
102 warn!("signing {} as a DMG", input_path.display());
103
104 let mut settings = self.settings.clone();
107
108 if settings.binary_identifier(SettingsScope::Main).is_none() {
109 let file_name = input_path
110 .file_stem()
111 .ok_or_else(|| {
112 AppleCodesignError::CliGeneralError("unable to resolve file name of DMG".into())
113 })?
114 .to_string_lossy();
115
116 warn!(
117 "setting binary identifier to {} (derived from file name)",
118 file_name
119 );
120 settings.set_binary_identifier(SettingsScope::Main, file_name);
121 }
122
123 if input_path != output_path {
128 info!(
129 "copying {} to {} in preparation for signing",
130 input_path.display(),
131 output_path.display()
132 );
133 if let Some(parent) = output_path.parent() {
134 std::fs::create_dir_all(parent)?;
135 }
136
137 std::fs::copy(input_path, output_path)?;
138 }
139
140 let signer = DmgSigner::default();
141 let mut fh = std::fs::File::options()
142 .read(true)
143 .write(true)
144 .open(output_path)?;
145 signer.sign_file(&settings, &mut fh)?;
146
147 Ok(())
148 }
149
150 pub fn sign_bundle(
152 &self,
153 input_path: impl AsRef<Path>,
154 output_path: impl AsRef<Path>,
155 ) -> Result<(), AppleCodesignError> {
156 let input_path = input_path.as_ref();
157 warn!("signing bundle at {}", input_path.display());
158
159 let mut signer = BundleSigner::new_from_path(input_path)?;
160 signer.collect_nested_bundles()?;
161 signer.write_signed_bundle(output_path, &self.settings)?;
162
163 Ok(())
164 }
165
166 pub fn sign_xar(
167 &self,
168 input_path: impl AsRef<Path>,
169 output_path: impl AsRef<Path>,
170 ) -> Result<(), AppleCodesignError> {
171 let input_path = input_path.as_ref();
172 let output_path = output_path.as_ref();
173
174 let output_path_temp =
178 output_path.with_file_name(if let Some(file_name) = output_path.file_name() {
179 file_name.to_string_lossy().to_string() + ".tmp"
180 } else {
181 "xar.tmp".to_string()
182 });
183
184 warn!(
185 "signing XAR pkg installer at {} to {}",
186 input_path.display(),
187 output_path_temp.display()
188 );
189
190 let (signing_key, signing_cert) = self
191 .settings
192 .signing_key()
193 .ok_or(AppleCodesignError::XarNoAdhoc)?;
194
195 {
196 let reader = XarReader::new(File::open(input_path)?)?;
197 let mut signer = XarSigner::new(reader);
198
199 let mut fh = File::create(&output_path_temp)?;
200 signer.sign(
201 &mut fh,
202 signing_key,
203 signing_cert,
204 self.settings.time_stamp_url(),
205 self.settings.certificate_chain().iter().cloned(),
206 )?;
207 }
208
209 if output_path.exists() {
210 warn!("removing existing {}", output_path.display());
211 std::fs::remove_file(output_path)?;
212 }
213
214 warn!(
215 "renaming {} -> {}",
216 output_path_temp.display(),
217 output_path.display()
218 );
219 std::fs::rename(&output_path_temp, output_path)?;
220
221 Ok(())
222 }
223}
224
225pub fn path_identifier(path: impl AsRef<Path>) -> Result<String, AppleCodesignError> {
226 let path = path.as_ref();
227
228 let file_name = path
230 .file_name()
231 .ok_or_else(|| {
232 AppleCodesignError::PathIdentifier(format!("path {} lacks a file name", path.display()))
233 })?
234 .to_string_lossy()
235 .to_string();
236
237 let id = if let Some((prefix, extension)) = file_name.rsplit_once('.') {
239 if extension.chars().all(|c| c.is_ascii_digit()) {
240 file_name.as_str()
241 } else {
242 prefix
243 }
244 } else {
245 file_name.as_str()
246 };
247
248 let is_digit_or_dot = |c: char| c == '.' || c.is_ascii_digit();
249
250 let id = match id.chars().next() {
253 Some(first) => {
254 if is_digit_or_dot(first) {
255 return Ok(id.to_string());
256 } else {
257 id
258 }
259 }
260 None => {
261 return Ok(id.to_string());
262 }
263 };
264
265 let prefix = id.trim_end_matches(is_digit_or_dot);
271 let stripped = &id[prefix.len()..];
272
273 if stripped.is_empty() {
274 Ok(id.to_string())
275 } else {
276 let (prefix, stripped) = if matches!(stripped.chars().next(), Some('.')) {
278 (&id[0..prefix.len() + 1], &stripped[1..])
279 } else {
280 (prefix, stripped)
281 };
282
283 let id = prefix
286 .chars()
287 .chain(stripped.chars().take_while(|c| c.is_ascii_digit()))
288 .collect::<String>();
289
290 Ok(id)
291 }
292}
293
294#[cfg(test)]
295mod test {
296 use super::*;
297 #[test]
298 fn path_identifier_normalization() {
299 assert_eq!(path_identifier("foo").unwrap(), "foo");
300 assert_eq!(path_identifier("foo.dylib").unwrap(), "foo");
301 assert_eq!(path_identifier("/etc/foo.dylib").unwrap(), "foo");
302 assert_eq!(path_identifier("/etc/foo").unwrap(), "foo");
303
304 assert_eq!(path_identifier(".foo").unwrap(), "");
306 assert_eq!(path_identifier("123").unwrap(), "123");
307 assert_eq!(path_identifier(".foo.dylib").unwrap(), ".foo");
308 assert_eq!(path_identifier("123.dylib").unwrap(), "123");
309 assert_eq!(path_identifier("123.42").unwrap(), "123.42");
310
311 assert_eq!(path_identifier("foo1").unwrap(), "foo1");
314 assert_eq!(path_identifier("foo1.dylib").unwrap(), "foo1");
315 assert_eq!(path_identifier("foo1.2.dylib").unwrap(), "foo1");
316 assert_eq!(path_identifier("foo1.2").unwrap(), "foo1");
317 assert_eq!(path_identifier("foo1.2.3.4.dylib").unwrap(), "foo1");
318 assert_eq!(path_identifier("foo.1").unwrap(), "foo.1");
319 assert_eq!(path_identifier("foo.1.2.3").unwrap(), "foo.1");
320 assert_eq!(path_identifier("foo.1.2.dylib").unwrap(), "foo.1");
321 assert_eq!(path_identifier("foo.1.dylib").unwrap(), "foo.1");
322 }
323}