apple_codesign/
verify.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Code signing verification.
6//!
7//! This module implements functionality for verifying code signatures on
8//! Mach-O binaries.
9//!
10//! # Verification Caveats
11//!
12//! **Verification performed by this code will vary from what Apple tools
13//! do. Do not use successful verification from this code as validation that
14//! Apple software will accept a signature.**
15//!
16//! We aim for our verification code to be as comprehensive as possible. But
17//! there are things it doesn't yet or won't ever do. For example, we have
18//! no clue of the full extent of verification that Apple performs because
19//! that code is proprietary. We know some of the things that are done and
20//! we have verification for a subset of them. Read the code or the set of
21//! verification problem types enumerated by [VerificationProblemType] to get
22//! a sense of what we do.
23
24use {
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/// Context for a verification issue.
37#[derive(Clone, Debug)]
38pub struct VerificationContext {
39    /// Path of binary.
40    pub path: Option<PathBuf>,
41
42    /// Index of Mach-O binary within a fat binary that is problematic.
43    pub fat_index: Option<usize>,
44}
45
46/// Describes a problem with verification.
47#[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
166/// Verifies unparsed Mach-O data.
167///
168/// Returns a vector of problems detected. An empty vector means no
169/// problems were found.
170pub 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
205/// Verifies a parsed Mach-O binary.
206///
207/// Returns a vector of problems detected. An empty vector means no
208/// problems were found.
209pub 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    // __LINKEDIT segment should be the last segment.
242    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    // Signature data should be the last data in the __LINKEDIT segment.
250    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            // Can't do anything more if we couldn't parse the signature data.
269            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        // TODO verify key length meets standards.
367        // TODO verify CA chain is fully present.
368        // TODO verify signing cert chains to Apple?
369    }
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    // All slots beneath some threshold should have a special hash.
434    // It isn't clear where this threshold is. But the alternate code directory and
435    // CMS slots appear to start at 0x1000. We set our limit at 32, which seems
436    // reasonable considering there are ~10 defined slots starting at value 0.
437    //
438    // The code directory doesn't have a digest because one cannot hash self.
439    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    // Verify the special/slot digests we do have match reality.
464    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                // Some slots have external provided from somewhere that isn't a blob.
488                if slot.has_external_content() {
489                    // TODO need to validate this external content somewhere.
490                }
491                // But slots with a null digest (all 0s) exist as placeholders when there
492                // is a higher numbered slot present.
493                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    // TODO verify code_limit[_64] is appropriate.
507    // TODO verify exec_seg_base is appropriate.
508
509    problems
510}