use {
crate::{
code_directory::CodeDirectoryBlob,
embedded_signature::{CodeSigningSlot, EmbeddedSignature},
error::AppleCodesignError,
macho::{MachFile, MachOBinary},
},
cryptographic_message_syntax::{CmsError, SignedData},
std::path::PathBuf,
x509_certificate::{DigestAlgorithm, SignatureAlgorithm},
};
#[derive(Clone, Debug)]
pub struct VerificationContext {
pub path: Option<PathBuf>,
pub fat_index: Option<usize>,
}
#[derive(Debug)]
pub enum VerificationProblemType {
IoError(std::io::Error),
MachOParseError(AppleCodesignError),
NoMachOSignatureData,
MachOSignatureError(AppleCodesignError),
LinkeditNotLastSegment,
SignatureNotLastLinkeditData,
NoCryptographicSignature,
CmsError(CmsError),
CmsOldDigestAlgorithm(DigestAlgorithm),
CmsOldSignatureAlgorithm(SignatureAlgorithm),
NoCodeDirectory,
CodeDigestError(AppleCodesignError),
CodeDigestMissingEntry(usize, Vec<u8>),
CodeDigestExtraEntry(usize, Vec<u8>),
CodeDigestMismatch(usize, Vec<u8>, Vec<u8>),
SlotDigestMissing(CodeSigningSlot),
ExtraSlotDigest(CodeSigningSlot, Vec<u8>),
SlotDigestMismatch(CodeSigningSlot, Vec<u8>, Vec<u8>),
SlotDigestError(AppleCodesignError),
}
#[derive(Debug)]
pub struct VerificationProblem {
pub context: VerificationContext,
pub problem: VerificationProblemType,
}
impl std::fmt::Display for VerificationProblem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let context = match (&self.context.path, &self.context.fat_index) {
(None, None) => None,
(Some(path), None) => Some(format!("{}", path.display())),
(None, Some(index)) => Some(format!("@{index}")),
(Some(path), Some(index)) => Some(format!("{}@{}", path.display(), index)),
};
let message = match &self.problem {
VerificationProblemType::IoError(e) => format!("I/O error: {e}"),
VerificationProblemType::MachOParseError(e) => format!("Mach-O parse failure: {e}"),
VerificationProblemType::NoMachOSignatureData => {
"Mach-O signature data not found".to_string()
}
VerificationProblemType::MachOSignatureError(e) => {
format!("error parsing Mach-O signature data: {e:?}")
}
VerificationProblemType::LinkeditNotLastSegment => {
"__LINKEDIT isn't last Mach-O segment".to_string()
}
VerificationProblemType::SignatureNotLastLinkeditData => {
"signature isn't last data in __LINKEDIT segment".to_string()
}
VerificationProblemType::NoCryptographicSignature => {
"no cryptographic signature present".to_string()
}
VerificationProblemType::CmsError(e) => format!("CMS error: {e}"),
VerificationProblemType::CmsOldDigestAlgorithm(alg) => {
format!("insecure digest algorithm used: {alg:?}")
}
VerificationProblemType::CmsOldSignatureAlgorithm(alg) => {
format!("insecure signature algorithm used: {alg:?}")
}
VerificationProblemType::NoCodeDirectory => "no code directory".to_string(),
VerificationProblemType::CodeDigestError(e) => {
format!("error computing code digests: {e:?}")
}
VerificationProblemType::CodeDigestMissingEntry(index, digest) => {
format!(
"code digest missing entry at index {} for digest {}",
index,
hex::encode(digest)
)
}
VerificationProblemType::CodeDigestExtraEntry(index, digest) => {
format!(
"code digest contains extra entry index {} with digest {}",
index,
hex::encode(digest)
)
}
VerificationProblemType::CodeDigestMismatch(index, cd_digest, actual_digest) => {
format!(
"code digest mismatch for entry {}; recorded digest {}, actual {}",
index,
hex::encode(cd_digest),
hex::encode(actual_digest)
)
}
VerificationProblemType::SlotDigestMissing(slot) => {
format!("missing digest for slot {slot:?}")
}
VerificationProblemType::ExtraSlotDigest(slot, digest) => {
format!(
"slot digest contains digest for slot not in signature: {:?} with digest {}",
slot,
hex::encode(digest)
)
}
VerificationProblemType::SlotDigestMismatch(slot, cd_digest, actual_digest) => {
format!(
"slot digest mismatch for slot {:?}; recorded digest {}, actual {}",
slot,
hex::encode(cd_digest),
hex::encode(actual_digest)
)
}
VerificationProblemType::SlotDigestError(e) => {
format!("error computing slot digest: {e:?}")
}
};
match context {
Some(context) => f.write_fmt(format_args!("{context}: {message}")),
None => f.write_str(&message),
}
}
}
pub fn verify_macho_data(data: impl AsRef<[u8]>) -> Vec<VerificationProblem> {
let context = VerificationContext {
path: None,
fat_index: None,
};
verify_macho_data_internal(data, context)
}
fn verify_macho_data_internal(
data: impl AsRef<[u8]>,
context: VerificationContext,
) -> Vec<VerificationProblem> {
match MachFile::parse(data.as_ref()) {
Ok(mach) => {
let mut problems = vec![];
for macho in mach.into_iter() {
let mut context = context.clone();
context.fat_index = macho.index;
problems.extend(verify_macho_internal(&macho, context));
}
problems
}
Err(e) => {
vec![VerificationProblem {
context,
problem: VerificationProblemType::MachOParseError(e),
}]
}
}
}
pub fn verify_macho(macho: &MachOBinary) -> Vec<VerificationProblem> {
verify_macho_internal(
macho,
VerificationContext {
path: None,
fat_index: None,
},
)
}
fn verify_macho_internal(
macho: &MachOBinary,
context: VerificationContext,
) -> Vec<VerificationProblem> {
let signature_data = match macho.find_signature_data() {
Ok(Some(data)) => data,
Ok(None) => {
return vec![VerificationProblem {
context,
problem: VerificationProblemType::NoMachOSignatureData,
}];
}
Err(e) => {
return vec![VerificationProblem {
context,
problem: VerificationProblemType::MachOSignatureError(e),
}];
}
};
let mut problems = vec![];
if signature_data.linkedit_segment_index != macho.macho.segments.len() - 1 {
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::LinkeditNotLastSegment,
});
}
if signature_data.signature_segment_end_offset != signature_data.linkedit_segment_data.len() {
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::SignatureNotLastLinkeditData,
});
}
let signature = match macho.code_signature() {
Ok(Some(signature)) => signature,
Ok(None) => {
panic!("no signature should have been handled above");
}
Err(e) => {
problems.push(VerificationProblem {
context,
problem: VerificationProblemType::MachOSignatureError(e),
});
return problems;
}
};
match signature.signature_data() {
Ok(Some(cms_blob)) => {
problems.extend(verify_cms_signature(cms_blob, context.clone()));
}
Ok(None) => problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::NoCryptographicSignature,
}),
Err(e) => {
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::MachOSignatureError(e),
});
}
}
match signature.code_directory() {
Ok(Some(cd)) => {
problems.extend(verify_code_directory(macho, &signature, &cd, context));
}
Ok(None) => {
problems.push(VerificationProblem {
context,
problem: VerificationProblemType::NoCodeDirectory,
});
}
Err(e) => {
problems.push(VerificationProblem {
context,
problem: VerificationProblemType::MachOSignatureError(e),
});
}
}
problems
}
fn verify_cms_signature(data: &[u8], context: VerificationContext) -> Vec<VerificationProblem> {
let signed_data = match SignedData::parse_ber(data) {
Ok(signed_data) => signed_data,
Err(e) => {
return vec![VerificationProblem {
context,
problem: VerificationProblemType::CmsError(e),
}];
}
};
let mut problems = vec![];
for signer in signed_data.signers() {
match signer.digest_algorithm() {
DigestAlgorithm::Sha1 => {
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::CmsOldDigestAlgorithm(
signer.digest_algorithm(),
),
});
}
DigestAlgorithm::Sha384 => {}
DigestAlgorithm::Sha256 => {}
DigestAlgorithm::Sha512 => {}
}
match signer.signature_algorithm() {
SignatureAlgorithm::RsaSha256
| SignatureAlgorithm::RsaSha384
| SignatureAlgorithm::RsaSha512
| SignatureAlgorithm::EcdsaSha256
| SignatureAlgorithm::EcdsaSha384
| SignatureAlgorithm::Ed25519
| SignatureAlgorithm::NoSignature(_) => {}
SignatureAlgorithm::RsaSha1 => {
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::CmsOldSignatureAlgorithm(
signer.signature_algorithm(),
),
});
}
}
match signer.verify_signature_with_signed_data(&signed_data) {
Ok(()) => {}
Err(e) => {
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::CmsError(e),
});
}
}
}
problems
}
fn verify_code_directory(
macho: &MachOBinary,
signature: &EmbeddedSignature,
cd: &CodeDirectoryBlob,
context: VerificationContext,
) -> Vec<VerificationProblem> {
let mut problems = vec![];
match macho.code_digests(cd.digest_type, cd.page_size as _) {
Ok(digests) => {
let mut cd_iter = cd.code_digests.iter().enumerate();
let mut actual_iter = digests.iter().enumerate();
loop {
match (cd_iter.next(), actual_iter.next()) {
(None, None) => {
break;
}
(Some((cd_index, cd_digest)), Some((_, actual_digest))) => {
if &cd_digest.data != actual_digest {
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::CodeDigestMismatch(
cd_index,
cd_digest.to_vec(),
actual_digest.clone(),
),
});
}
}
(None, Some((actual_index, actual_digest))) => {
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::CodeDigestMissingEntry(
actual_index,
actual_digest.clone(),
),
});
}
(Some((cd_index, cd_digest)), None) => {
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::CodeDigestExtraEntry(
cd_index,
cd_digest.to_vec(),
),
});
}
}
}
}
Err(e) => {
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::CodeDigestError(e),
});
}
}
for blob in &signature.blobs {
let slot = blob.slot;
if u32::from(slot) < 32
&& !cd.slot_digests().contains_key(&slot)
&& slot != CodeSigningSlot::CodeDirectory
{
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::SlotDigestMissing(slot),
});
}
}
let max_slot = cd
.slot_digests()
.keys()
.map(|slot| u32::from(*slot))
.filter(|slot| *slot < 32)
.max()
.unwrap_or(0);
let null_digest = b"\0".repeat(cd.digest_size as usize);
for (slot, cd_digest) in cd.slot_digests().iter() {
match signature.find_slot(*slot) {
Some(entry) => match entry.digest_with(cd.digest_type) {
Ok(actual_digest) => {
if actual_digest != cd_digest.to_vec() {
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::SlotDigestMismatch(
*slot,
cd_digest.to_vec(),
actual_digest,
),
});
}
}
Err(e) => {
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::SlotDigestError(e),
});
}
},
None => {
if slot.has_external_content() {
}
else if u32::from(*slot) >= max_slot || cd_digest.to_vec() != null_digest {
problems.push(VerificationProblem {
context: context.clone(),
problem: VerificationProblemType::ExtraSlotDigest(
*slot,
cd_digest.to_vec(),
),
});
}
}
}
}
problems
}