1use {
15 crate::{
16 bundle_signing::SignedMachOInfo,
17 cryptography::DigestType,
18 dmg::{DmgReader, DmgSigner},
19 embedded_signature::Blob,
20 reader::PathType,
21 ticket_lookup::{default_client, lookup_notarization_ticket},
22 AppleCodesignError,
23 },
24 apple_bundles::DirectoryBundle,
25 apple_xar::reader::XarReader,
26 log::{error, info, warn},
27 reqwest::blocking::Client,
28 scroll::{IOread, IOwrite, Pread, Pwrite, SizeWith},
29 std::{
30 fmt::Debug,
31 fs::File,
32 io::{Read, Seek, SeekFrom, Write},
33 path::Path,
34 },
35};
36
37pub fn record_name_from_executable_bundle(
42 bundle: &DirectoryBundle,
43) -> Result<String, AppleCodesignError> {
44 let main_exe = bundle
45 .files(false)
46 .map_err(AppleCodesignError::DirectoryBundle)?
47 .into_iter()
48 .find(|file| matches!(file.is_main_executable(), Ok(true)))
49 .ok_or(AppleCodesignError::StapleMainExecutableNotFound)?;
50
51 info!(
53 "resolving bundle's record name from {}",
54 main_exe.absolute_path().display()
55 );
56 let macho_data = std::fs::read(main_exe.absolute_path())?;
57
58 let signed = SignedMachOInfo::parse_data(&macho_data)?;
59
60 let record_name = signed.notarization_ticket_record_name()?;
61
62 Ok(record_name)
63}
64
65pub fn staple_ticket_to_bundle(
70 bundle: &DirectoryBundle,
71 ticket_data: &[u8],
72) -> Result<(), AppleCodesignError> {
73 let path = bundle.resolve_path("CodeResources");
74
75 warn!("writing notarization ticket to {}", path.display());
76 std::fs::write(&path, ticket_data)?;
77
78 Ok(())
79}
80
81const XAR_NOTARIZATION_TRAILER_MAGIC: [u8; 4] = [0x74, 0x38, 0x6c, 0x72];
85
86#[derive(Clone, Copy, Debug, IOread, IOwrite, Pread, Pwrite, SizeWith)]
87pub struct XarNotarizationTrailer {
88 pub magic: [u8; 4],
90 pub version: u16,
91 pub typ: u16,
92 pub length: u32,
93 pub unused: u32,
94}
95
96#[derive(Clone, Copy, Debug)]
97#[repr(u16)]
98pub enum XarNotarizationTrailerType {
99 Invalid = 0,
100 Terminator = 1,
101 Ticket = 2,
102}
103
104pub fn xar_notarization_trailer(ticket_data: &[u8]) -> Result<Vec<u8>, AppleCodesignError> {
112 let terminator = XarNotarizationTrailer {
113 magic: XAR_NOTARIZATION_TRAILER_MAGIC,
114 version: 1,
115 typ: XarNotarizationTrailerType::Terminator as u16,
116 length: 0,
117 unused: 0,
118 };
119 let ticket = XarNotarizationTrailer {
120 magic: XAR_NOTARIZATION_TRAILER_MAGIC,
121 version: 1,
122 typ: XarNotarizationTrailerType::Ticket as u16,
123 length: ticket_data.len() as _,
124 unused: 0,
125 };
126
127 let mut cursor = std::io::Cursor::new(Vec::new());
128 cursor.iowrite_with(terminator, scroll::LE)?;
129 cursor.write_all(ticket_data)?;
130 cursor.iowrite_with(ticket, scroll::LE)?;
131
132 Ok(cursor.into_inner())
133}
134
135pub struct Stapler {
137 client: Client,
138}
139
140impl Stapler {
141 pub fn new() -> Result<Self, AppleCodesignError> {
143 Ok(Self {
144 client: default_client()?,
145 })
146 }
147
148 pub fn set_client(&mut self, client: Client) {
150 self.client = client;
151 }
152
153 pub fn lookup_ticket_for_executable_bundle(
163 &self,
164 bundle: &DirectoryBundle,
165 ) -> Result<Vec<u8>, AppleCodesignError> {
166 let record_name = record_name_from_executable_bundle(bundle)?;
167
168 let response = lookup_notarization_ticket(&self.client, &record_name)?;
169
170 let ticket_data = response.signed_ticket(&record_name)?;
171
172 Ok(ticket_data)
173 }
174
175 pub fn staple_bundle(&self, bundle: &DirectoryBundle) -> Result<(), AppleCodesignError> {
177 warn!(
178 "attempting to find notarization ticket for bundle at {}",
179 bundle.root_dir().display()
180 );
181 let ticket_data = self.lookup_ticket_for_executable_bundle(bundle)?;
182 staple_ticket_to_bundle(bundle, &ticket_data)?;
183
184 Ok(())
185 }
186
187 pub fn lookup_ticket_for_dmg(&self, dmg: &DmgReader) -> Result<Vec<u8>, AppleCodesignError> {
189 let signature = dmg
192 .embedded_signature()?
193 .ok_or(AppleCodesignError::DmgStapleNoSignature)?;
194 let cd = signature
195 .code_directory()?
196 .ok_or(AppleCodesignError::DmgStapleNoSignature)?;
197
198 let mut digest = cd.digest_with(cd.digest_type)?;
199 digest.truncate(20);
200 let digest = hex::encode(digest);
201
202 let digest_type: u8 = cd.digest_type.into();
203
204 let record_name = format!("2/{digest_type}/{digest}");
205
206 let response = lookup_notarization_ticket(&self.client, &record_name)?;
207
208 response.signed_ticket(&record_name)
209 }
210
211 pub fn staple_dmg(&self, path: &Path) -> Result<(), AppleCodesignError> {
213 let mut fh = File::options().read(true).write(true).open(path)?;
214
215 warn!(
216 "attempting to find notarization ticket for DMG at {}",
217 path.display()
218 );
219 let reader = DmgReader::new(&mut fh)?;
220
221 let ticket_data = self.lookup_ticket_for_dmg(&reader)?;
222 warn!("found notarization ticket; proceeding with stapling");
223
224 let signer = DmgSigner::default();
225 signer.staple_file(&mut fh, ticket_data)?;
226
227 Ok(())
228 }
229
230 pub fn lookup_ticket_for_xar<R: Read + Seek + Sized + Debug>(
232 &self,
233 reader: &mut XarReader<R>,
234 ) -> Result<Vec<u8>, AppleCodesignError> {
235 let mut digest = reader.checksum_data()?;
236 digest.truncate(20);
237 let digest = hex::encode(digest);
238
239 let digest_type = DigestType::try_from(reader.table_of_contents().checksum.style)?;
240 let digest_type: u8 = digest_type.into();
241
242 let record_name = format!("2/{digest_type}/{digest}");
243
244 let response = lookup_notarization_ticket(&self.client, &record_name)?;
245
246 response.signed_ticket(&record_name)
247 }
248
249 pub fn staple_xar<F: Read + Write + Seek + Sized + Debug>(
256 &self,
257 mut xar: XarReader<F>,
258 ) -> Result<(), AppleCodesignError> {
259 let ticket_data = self.lookup_ticket_for_xar(&mut xar)?;
260
261 warn!("found notarization ticket; proceeding with stapling");
262
263 let mut fh = xar.into_inner();
264
265 let trailer_size = 16;
271 fh.seek(SeekFrom::End(-trailer_size))?;
272
273 let trailer = fh.ioread_with::<XarNotarizationTrailer>(scroll::LE)?;
274 if trailer.magic == XAR_NOTARIZATION_TRAILER_MAGIC {
275 let trailer_type = match trailer.typ {
276 x if x == XarNotarizationTrailerType::Invalid as u16 => "invalid",
277 x if x == XarNotarizationTrailerType::Ticket as u16 => "ticket",
278 x if x == XarNotarizationTrailerType::Terminator as u16 => "terminator",
279 _ => "unknown",
280 };
281
282 warn!("found an existing XAR trailer of type {}", trailer_type);
283 warn!("this existing trailer will be preserved and will likely be ignored");
284 }
285
286 let trailer = xar_notarization_trailer(&ticket_data)?;
287
288 warn!(
289 "stapling notarization ticket trailer ({} bytes) to end of XAR",
290 trailer.len()
291 );
292 fh.write_all(&trailer)?;
293
294 Ok(())
295 }
296
297 pub fn staple_path(&self, path: impl AsRef<Path>) -> Result<(), AppleCodesignError> {
301 let path = path.as_ref();
302 warn!("attempting to staple {}", path.display());
303
304 match PathType::from_path(path)? {
305 PathType::MachO => {
306 error!("cannot staple Mach-O binaries");
307 Err(AppleCodesignError::StapleUnsupportedPath(
308 path.to_path_buf(),
309 ))
310 }
311 PathType::Dmg => {
312 warn!("activating DMG stapling mode");
313 self.staple_dmg(path)
314 }
315 PathType::Bundle => {
316 warn!("activating bundle stapling mode");
317 let bundle = DirectoryBundle::new_from_path(path)
318 .map_err(AppleCodesignError::DirectoryBundle)?;
319 self.staple_bundle(&bundle)
320 }
321 PathType::Xar => {
322 warn!("activating XAR stapling mode");
323 let xar = XarReader::new(File::options().read(true).write(true).open(path)?)?;
324 self.staple_xar(xar)
325 }
326 PathType::Zip | PathType::Other => Err(AppleCodesignError::StapleUnsupportedPath(
327 path.to_path_buf(),
328 )),
329 }
330 }
331}