1use {
25 crate::{
26 code_directory::CodeDirectoryBlob,
27 embedded_signature::{CodeSigningSlot, EmbeddedSignature},
28 error::AppleCodesignError,
29 macho::{MachFile, MachOBinary},
30 },
31 cryptographic_message_syntax::{CmsError, SignedData},
32 std::path::PathBuf,
33 x509_certificate::{DigestAlgorithm, SignatureAlgorithm},
34};
35
36#[derive(Clone, Debug)]
38pub struct VerificationContext {
39 pub path: Option<PathBuf>,
41
42 pub fat_index: Option<usize>,
44}
45
46#[derive(Debug)]
48pub enum VerificationProblemType {
49 IoError(std::io::Error),
50 MachOParseError(AppleCodesignError),
51 NoMachOSignatureData,
52 MachOSignatureError(AppleCodesignError),
53 LinkeditNotLastSegment,
54 SignatureNotLastLinkeditData,
55 NoCryptographicSignature,
56 CmsError(CmsError),
57 CmsOldDigestAlgorithm(DigestAlgorithm),
58 CmsOldSignatureAlgorithm(SignatureAlgorithm),
59 NoCodeDirectory,
60 CodeDigestError(AppleCodesignError),
61 CodeDigestMissingEntry(usize, Vec<u8>),
62 CodeDigestExtraEntry(usize, Vec<u8>),
63 CodeDigestMismatch(usize, Vec<u8>, Vec<u8>),
64 SlotDigestMissing(CodeSigningSlot),
65 ExtraSlotDigest(CodeSigningSlot, Vec<u8>),
66 SlotDigestMismatch(CodeSigningSlot, Vec<u8>, Vec<u8>),
67 SlotDigestError(AppleCodesignError),
68}
69
70#[derive(Debug)]
71pub struct VerificationProblem {
72 pub context: VerificationContext,
73 pub problem: VerificationProblemType,
74}
75
76impl std::fmt::Display for VerificationProblem {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 let context = match (&self.context.path, &self.context.fat_index) {
79 (None, None) => None,
80 (Some(path), None) => Some(format!("{}", path.display())),
81 (None, Some(index)) => Some(format!("@{index}")),
82 (Some(path), Some(index)) => Some(format!("{}@{}", path.display(), index)),
83 };
84
85 let message = match &self.problem {
86 VerificationProblemType::IoError(e) => format!("I/O error: {e}"),
87 VerificationProblemType::MachOParseError(e) => format!("Mach-O parse failure: {e}"),
88 VerificationProblemType::NoMachOSignatureData => {
89 "Mach-O signature data not found".to_string()
90 }
91 VerificationProblemType::MachOSignatureError(e) => {
92 format!("error parsing Mach-O signature data: {e:?}")
93 }
94 VerificationProblemType::LinkeditNotLastSegment => {
95 "__LINKEDIT isn't last Mach-O segment".to_string()
96 }
97 VerificationProblemType::SignatureNotLastLinkeditData => {
98 "signature isn't last data in __LINKEDIT segment".to_string()
99 }
100 VerificationProblemType::NoCryptographicSignature => {
101 "no cryptographic signature present".to_string()
102 }
103 VerificationProblemType::CmsError(e) => format!("CMS error: {e}"),
104 VerificationProblemType::CmsOldDigestAlgorithm(alg) => {
105 format!("insecure digest algorithm used: {alg:?}")
106 }
107 VerificationProblemType::CmsOldSignatureAlgorithm(alg) => {
108 format!("insecure signature algorithm used: {alg:?}")
109 }
110 VerificationProblemType::NoCodeDirectory => "no code directory".to_string(),
111 VerificationProblemType::CodeDigestError(e) => {
112 format!("error computing code digests: {e:?}")
113 }
114 VerificationProblemType::CodeDigestMissingEntry(index, digest) => {
115 format!(
116 "code digest missing entry at index {} for digest {}",
117 index,
118 hex::encode(digest)
119 )
120 }
121 VerificationProblemType::CodeDigestExtraEntry(index, digest) => {
122 format!(
123 "code digest contains extra entry index {} with digest {}",
124 index,
125 hex::encode(digest)
126 )
127 }
128 VerificationProblemType::CodeDigestMismatch(index, cd_digest, actual_digest) => {
129 format!(
130 "code digest mismatch for entry {}; recorded digest {}, actual {}",
131 index,
132 hex::encode(cd_digest),
133 hex::encode(actual_digest)
134 )
135 }
136 VerificationProblemType::SlotDigestMissing(slot) => {
137 format!("missing digest for slot {slot:?}")
138 }
139 VerificationProblemType::ExtraSlotDigest(slot, digest) => {
140 format!(
141 "slot digest contains digest for slot not in signature: {:?} with digest {}",
142 slot,
143 hex::encode(digest)
144 )
145 }
146 VerificationProblemType::SlotDigestMismatch(slot, cd_digest, actual_digest) => {
147 format!(
148 "slot digest mismatch for slot {:?}; recorded digest {}, actual {}",
149 slot,
150 hex::encode(cd_digest),
151 hex::encode(actual_digest)
152 )
153 }
154 VerificationProblemType::SlotDigestError(e) => {
155 format!("error computing slot digest: {e:?}")
156 }
157 };
158
159 match context {
160 Some(context) => f.write_fmt(format_args!("{context}: {message}")),
161 None => f.write_str(&message),
162 }
163 }
164}
165
166pub fn verify_macho_data(data: impl AsRef<[u8]>) -> Vec<VerificationProblem> {
171 let context = VerificationContext {
172 path: None,
173 fat_index: None,
174 };
175
176 verify_macho_data_internal(data, context)
177}
178
179fn verify_macho_data_internal(
180 data: impl AsRef<[u8]>,
181 context: VerificationContext,
182) -> Vec<VerificationProblem> {
183 match MachFile::parse(data.as_ref()) {
184 Ok(mach) => {
185 let mut problems = vec![];
186
187 for macho in mach.into_iter() {
188 let mut context = context.clone();
189 context.fat_index = macho.index;
190
191 problems.extend(verify_macho_internal(&macho, context));
192 }
193
194 problems
195 }
196 Err(e) => {
197 vec![VerificationProblem {
198 context,
199 problem: VerificationProblemType::MachOParseError(e),
200 }]
201 }
202 }
203}
204
205pub fn verify_macho(macho: &MachOBinary) -> Vec<VerificationProblem> {
210 verify_macho_internal(
211 macho,
212 VerificationContext {
213 path: None,
214 fat_index: None,
215 },
216 )
217}
218
219fn verify_macho_internal(
220 macho: &MachOBinary,
221 context: VerificationContext,
222) -> Vec<VerificationProblem> {
223 let signature_data = match macho.find_signature_data() {
224 Ok(Some(data)) => data,
225 Ok(None) => {
226 return vec![VerificationProblem {
227 context,
228 problem: VerificationProblemType::NoMachOSignatureData,
229 }];
230 }
231 Err(e) => {
232 return vec![VerificationProblem {
233 context,
234 problem: VerificationProblemType::MachOSignatureError(e),
235 }];
236 }
237 };
238
239 let mut problems = vec![];
240
241 if signature_data.linkedit_segment_index != macho.macho.segments.len() - 1 {
243 problems.push(VerificationProblem {
244 context: context.clone(),
245 problem: VerificationProblemType::LinkeditNotLastSegment,
246 });
247 }
248
249 if signature_data.signature_segment_end_offset != signature_data.linkedit_segment_data.len() {
251 problems.push(VerificationProblem {
252 context: context.clone(),
253 problem: VerificationProblemType::SignatureNotLastLinkeditData,
254 });
255 }
256
257 let signature = match macho.code_signature() {
258 Ok(Some(signature)) => signature,
259 Ok(None) => {
260 panic!("no signature should have been handled above");
261 }
262 Err(e) => {
263 problems.push(VerificationProblem {
264 context,
265 problem: VerificationProblemType::MachOSignatureError(e),
266 });
267
268 return problems;
270 }
271 };
272
273 match signature.signature_data() {
274 Ok(Some(cms_blob)) => {
275 problems.extend(verify_cms_signature(cms_blob, context.clone()));
276 }
277 Ok(None) => problems.push(VerificationProblem {
278 context: context.clone(),
279 problem: VerificationProblemType::NoCryptographicSignature,
280 }),
281 Err(e) => {
282 problems.push(VerificationProblem {
283 context: context.clone(),
284 problem: VerificationProblemType::MachOSignatureError(e),
285 });
286 }
287 }
288
289 match signature.code_directory() {
290 Ok(Some(cd)) => {
291 problems.extend(verify_code_directory(macho, &signature, &cd, context));
292 }
293 Ok(None) => {
294 problems.push(VerificationProblem {
295 context,
296 problem: VerificationProblemType::NoCodeDirectory,
297 });
298 }
299 Err(e) => {
300 problems.push(VerificationProblem {
301 context,
302 problem: VerificationProblemType::MachOSignatureError(e),
303 });
304 }
305 }
306
307 problems
308}
309
310fn verify_cms_signature(data: &[u8], context: VerificationContext) -> Vec<VerificationProblem> {
311 let signed_data = match SignedData::parse_ber(data) {
312 Ok(signed_data) => signed_data,
313 Err(e) => {
314 return vec![VerificationProblem {
315 context,
316 problem: VerificationProblemType::CmsError(e),
317 }];
318 }
319 };
320
321 let mut problems = vec![];
322
323 for signer in signed_data.signers() {
324 match signer.digest_algorithm() {
325 DigestAlgorithm::Sha1 => {
326 problems.push(VerificationProblem {
327 context: context.clone(),
328 problem: VerificationProblemType::CmsOldDigestAlgorithm(
329 signer.digest_algorithm(),
330 ),
331 });
332 }
333 DigestAlgorithm::Sha384 => {}
334 DigestAlgorithm::Sha256 => {}
335 DigestAlgorithm::Sha512 => {}
336 }
337
338 match signer.signature_algorithm() {
339 SignatureAlgorithm::RsaSha256
340 | SignatureAlgorithm::RsaSha384
341 | SignatureAlgorithm::RsaSha512
342 | SignatureAlgorithm::EcdsaSha256
343 | SignatureAlgorithm::EcdsaSha384
344 | SignatureAlgorithm::Ed25519
345 | SignatureAlgorithm::NoSignature(_) => {}
346 SignatureAlgorithm::RsaSha1 => {
347 problems.push(VerificationProblem {
348 context: context.clone(),
349 problem: VerificationProblemType::CmsOldSignatureAlgorithm(
350 signer.signature_algorithm(),
351 ),
352 });
353 }
354 }
355
356 match signer.verify_signature_with_signed_data(&signed_data) {
357 Ok(()) => {}
358 Err(e) => {
359 problems.push(VerificationProblem {
360 context: context.clone(),
361 problem: VerificationProblemType::CmsError(e),
362 });
363 }
364 }
365
366 }
370
371 problems
372}
373
374fn verify_code_directory(
375 macho: &MachOBinary,
376 signature: &EmbeddedSignature,
377 cd: &CodeDirectoryBlob,
378 context: VerificationContext,
379) -> Vec<VerificationProblem> {
380 let mut problems = vec![];
381
382 match macho.code_digests(cd.digest_type, cd.page_size as _) {
383 Ok(digests) => {
384 let mut cd_iter = cd.code_digests.iter().enumerate();
385 let mut actual_iter = digests.iter().enumerate();
386
387 loop {
388 match (cd_iter.next(), actual_iter.next()) {
389 (None, None) => {
390 break;
391 }
392 (Some((cd_index, cd_digest)), Some((_, actual_digest))) => {
393 if &cd_digest.data != actual_digest {
394 problems.push(VerificationProblem {
395 context: context.clone(),
396 problem: VerificationProblemType::CodeDigestMismatch(
397 cd_index,
398 cd_digest.to_vec(),
399 actual_digest.clone(),
400 ),
401 });
402 }
403 }
404 (None, Some((actual_index, actual_digest))) => {
405 problems.push(VerificationProblem {
406 context: context.clone(),
407 problem: VerificationProblemType::CodeDigestMissingEntry(
408 actual_index,
409 actual_digest.clone(),
410 ),
411 });
412 }
413 (Some((cd_index, cd_digest)), None) => {
414 problems.push(VerificationProblem {
415 context: context.clone(),
416 problem: VerificationProblemType::CodeDigestExtraEntry(
417 cd_index,
418 cd_digest.to_vec(),
419 ),
420 });
421 }
422 }
423 }
424 }
425 Err(e) => {
426 problems.push(VerificationProblem {
427 context: context.clone(),
428 problem: VerificationProblemType::CodeDigestError(e),
429 });
430 }
431 }
432
433 for blob in &signature.blobs {
440 let slot = blob.slot;
441
442 if u32::from(slot) < 32
443 && !cd.slot_digests().contains_key(&slot)
444 && slot != CodeSigningSlot::CodeDirectory
445 {
446 problems.push(VerificationProblem {
447 context: context.clone(),
448 problem: VerificationProblemType::SlotDigestMissing(slot),
449 });
450 }
451 }
452
453 let max_slot = cd
454 .slot_digests()
455 .keys()
456 .map(|slot| u32::from(*slot))
457 .filter(|slot| *slot < 32)
458 .max()
459 .unwrap_or(0);
460
461 let null_digest = b"\0".repeat(cd.digest_size as usize);
462
463 for (slot, cd_digest) in cd.slot_digests().iter() {
465 match signature.find_slot(*slot) {
466 Some(entry) => match entry.digest_with(cd.digest_type) {
467 Ok(actual_digest) => {
468 if actual_digest != cd_digest.to_vec() {
469 problems.push(VerificationProblem {
470 context: context.clone(),
471 problem: VerificationProblemType::SlotDigestMismatch(
472 *slot,
473 cd_digest.to_vec(),
474 actual_digest,
475 ),
476 });
477 }
478 }
479 Err(e) => {
480 problems.push(VerificationProblem {
481 context: context.clone(),
482 problem: VerificationProblemType::SlotDigestError(e),
483 });
484 }
485 },
486 None => {
487 if slot.has_external_content() {
489 }
491 else if u32::from(*slot) >= max_slot || cd_digest.to_vec() != null_digest {
494 problems.push(VerificationProblem {
495 context: context.clone(),
496 problem: VerificationProblemType::ExtraSlotDigest(
497 *slot,
498 cd_digest.to_vec(),
499 ),
500 });
501 }
502 }
503 }
504 }
505
506 problems
510}