use {
crate::{
bundle_signing::SignedMachOInfo,
cryptography::DigestType,
dmg::{DmgReader, DmgSigner},
embedded_signature::Blob,
reader::PathType,
ticket_lookup::{default_client, lookup_notarization_ticket},
AppleCodesignError,
},
apple_bundles::DirectoryBundle,
apple_xar::reader::XarReader,
log::{error, info, warn},
reqwest::blocking::Client,
scroll::{IOread, IOwrite, Pread, Pwrite, SizeWith},
std::{
fmt::Debug,
fs::File,
io::{Read, Seek, SeekFrom, Write},
path::Path,
},
};
pub fn record_name_from_executable_bundle(
bundle: &DirectoryBundle,
) -> Result<String, AppleCodesignError> {
let main_exe = bundle
.files(false)
.map_err(AppleCodesignError::DirectoryBundle)?
.into_iter()
.find(|file| matches!(file.is_main_executable(), Ok(true)))
.ok_or(AppleCodesignError::StapleMainExecutableNotFound)?;
info!(
"resolving bundle's record name from {}",
main_exe.absolute_path().display()
);
let macho_data = std::fs::read(main_exe.absolute_path())?;
let signed = SignedMachOInfo::parse_data(&macho_data)?;
let record_name = signed.notarization_ticket_record_name()?;
Ok(record_name)
}
pub fn staple_ticket_to_bundle(
bundle: &DirectoryBundle,
ticket_data: &[u8],
) -> Result<(), AppleCodesignError> {
let path = bundle.resolve_path("CodeResources");
warn!("writing notarization ticket to {}", path.display());
std::fs::write(&path, ticket_data)?;
Ok(())
}
const XAR_NOTARIZATION_TRAILER_MAGIC: [u8; 4] = [0x74, 0x38, 0x6c, 0x72];
#[derive(Clone, Copy, Debug, IOread, IOwrite, Pread, Pwrite, SizeWith)]
pub struct XarNotarizationTrailer {
pub magic: [u8; 4],
pub version: u16,
pub typ: u16,
pub length: u32,
pub unused: u32,
}
#[derive(Clone, Copy, Debug)]
#[repr(u16)]
pub enum XarNotarizationTrailerType {
Invalid = 0,
Terminator = 1,
Ticket = 2,
}
pub fn xar_notarization_trailer(ticket_data: &[u8]) -> Result<Vec<u8>, AppleCodesignError> {
let terminator = XarNotarizationTrailer {
magic: XAR_NOTARIZATION_TRAILER_MAGIC,
version: 1,
typ: XarNotarizationTrailerType::Terminator as u16,
length: 0,
unused: 0,
};
let ticket = XarNotarizationTrailer {
magic: XAR_NOTARIZATION_TRAILER_MAGIC,
version: 1,
typ: XarNotarizationTrailerType::Ticket as u16,
length: ticket_data.len() as _,
unused: 0,
};
let mut cursor = std::io::Cursor::new(Vec::new());
cursor.iowrite_with(terminator, scroll::LE)?;
cursor.write_all(ticket_data)?;
cursor.iowrite_with(ticket, scroll::LE)?;
Ok(cursor.into_inner())
}
pub struct Stapler {
client: Client,
}
impl Stapler {
pub fn new() -> Result<Self, AppleCodesignError> {
Ok(Self {
client: default_client()?,
})
}
pub fn set_client(&mut self, client: Client) {
self.client = client;
}
pub fn lookup_ticket_for_executable_bundle(
&self,
bundle: &DirectoryBundle,
) -> Result<Vec<u8>, AppleCodesignError> {
let record_name = record_name_from_executable_bundle(bundle)?;
let response = lookup_notarization_ticket(&self.client, &record_name)?;
let ticket_data = response.signed_ticket(&record_name)?;
Ok(ticket_data)
}
pub fn staple_bundle(&self, bundle: &DirectoryBundle) -> Result<(), AppleCodesignError> {
warn!(
"attempting to find notarization ticket for bundle at {}",
bundle.root_dir().display()
);
let ticket_data = self.lookup_ticket_for_executable_bundle(bundle)?;
staple_ticket_to_bundle(bundle, &ticket_data)?;
Ok(())
}
pub fn lookup_ticket_for_dmg(&self, dmg: &DmgReader) -> Result<Vec<u8>, AppleCodesignError> {
let signature = dmg
.embedded_signature()?
.ok_or(AppleCodesignError::DmgStapleNoSignature)?;
let cd = signature
.code_directory()?
.ok_or(AppleCodesignError::DmgStapleNoSignature)?;
let mut digest = cd.digest_with(cd.digest_type)?;
digest.truncate(20);
let digest = hex::encode(digest);
let digest_type: u8 = cd.digest_type.into();
let record_name = format!("2/{digest_type}/{digest}");
let response = lookup_notarization_ticket(&self.client, &record_name)?;
response.signed_ticket(&record_name)
}
pub fn staple_dmg(&self, path: &Path) -> Result<(), AppleCodesignError> {
let mut fh = File::options().read(true).write(true).open(path)?;
warn!(
"attempting to find notarization ticket for DMG at {}",
path.display()
);
let reader = DmgReader::new(&mut fh)?;
let ticket_data = self.lookup_ticket_for_dmg(&reader)?;
warn!("found notarization ticket; proceeding with stapling");
let signer = DmgSigner::default();
signer.staple_file(&mut fh, ticket_data)?;
Ok(())
}
pub fn lookup_ticket_for_xar<R: Read + Seek + Sized + Debug>(
&self,
reader: &mut XarReader<R>,
) -> Result<Vec<u8>, AppleCodesignError> {
let mut digest = reader.checksum_data()?;
digest.truncate(20);
let digest = hex::encode(digest);
let digest_type = DigestType::try_from(reader.table_of_contents().checksum.style)?;
let digest_type: u8 = digest_type.into();
let record_name = format!("2/{digest_type}/{digest}");
let response = lookup_notarization_ticket(&self.client, &record_name)?;
response.signed_ticket(&record_name)
}
pub fn staple_xar<F: Read + Write + Seek + Sized + Debug>(
&self,
mut xar: XarReader<F>,
) -> Result<(), AppleCodesignError> {
let ticket_data = self.lookup_ticket_for_xar(&mut xar)?;
warn!("found notarization ticket; proceeding with stapling");
let mut fh = xar.into_inner();
let trailer_size = 16;
fh.seek(SeekFrom::End(-trailer_size))?;
let trailer = fh.ioread_with::<XarNotarizationTrailer>(scroll::LE)?;
if trailer.magic == XAR_NOTARIZATION_TRAILER_MAGIC {
let trailer_type = match trailer.typ {
x if x == XarNotarizationTrailerType::Invalid as u16 => "invalid",
x if x == XarNotarizationTrailerType::Ticket as u16 => "ticket",
x if x == XarNotarizationTrailerType::Terminator as u16 => "terminator",
_ => "unknown",
};
warn!("found an existing XAR trailer of type {}", trailer_type);
warn!("this existing trailer will be preserved and will likely be ignored");
}
let trailer = xar_notarization_trailer(&ticket_data)?;
warn!(
"stapling notarization ticket trailer ({} bytes) to end of XAR",
trailer.len()
);
fh.write_all(&trailer)?;
Ok(())
}
pub fn staple_path(&self, path: impl AsRef<Path>) -> Result<(), AppleCodesignError> {
let path = path.as_ref();
warn!("attempting to staple {}", path.display());
match PathType::from_path(path)? {
PathType::MachO => {
error!("cannot staple Mach-O binaries");
Err(AppleCodesignError::StapleUnsupportedPath(
path.to_path_buf(),
))
}
PathType::Dmg => {
warn!("activating DMG stapling mode");
self.staple_dmg(path)
}
PathType::Bundle => {
warn!("activating bundle stapling mode");
let bundle = DirectoryBundle::new_from_path(path)
.map_err(AppleCodesignError::DirectoryBundle)?;
self.staple_bundle(&bundle)
}
PathType::Xar => {
warn!("activating XAR stapling mode");
let xar = XarReader::new(File::options().read(true).write(true).open(path)?)?;
self.staple_xar(xar)
}
PathType::Zip | PathType::Other => Err(AppleCodesignError::StapleUnsupportedPath(
path.to_path_buf(),
)),
}
}
}