use {
cryptographic_message_syntax::CmsError,
reqwest::{IntoUrl, Url},
slog::warn,
std::{
borrow::Cow,
convert::TryFrom,
ops::Deref,
path::{Path, PathBuf},
sync::Arc,
},
thiserror::Error,
tugger_apple_codesign::{AppleCodesignError, MachOSigner},
tugger_file_manifest::{File, FileData, FileEntry},
tugger_windows_codesign::{
CodeSigningCertificate, FileBasedCodeSigningCertificate, SystemStore,
},
x509_certificate::{CapturedX509Certificate, InMemorySigningKeyPair, X509CertificateError},
yasna::ASN1Error,
};
pub const APPLE_TIMESTAMP_URL: &str = "http://timestamp.apple.com/ts01";
#[derive(Debug, Error)]
pub enum SigningError {
#[error("could not determine if path is signable: {0}")]
SignableTestError(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("error reading ASN.1 data: {0}")]
Asn1(#[from] ASN1Error),
#[error("cryptography error: {0}")]
Cms(#[from] CmsError),
#[error("no certificate data was found")]
NoCertificateData,
#[error("incorrect decryption password")]
BadDecryptionPassword,
#[error("PFX reading error: {0}")]
PfxRead(String),
#[error("{0}")]
BadWindowsCertificateStore(String),
#[error("bad URL: {0}")]
BadUrl(reqwest::Error),
#[error("macOS keychain integration only supported on macOS")]
MacOsKeychainNotSupported,
#[error("failed to resolve signing certificate: {0}")]
CertificateResolutionFailure(String),
#[error("certificate not usable: {0}")]
CertificateNotUsable(String),
#[error("error resolving certificate chain: {0}")]
MacOsCertificateChainResolveFailure(AppleCodesignError),
#[error("path {0} is not signable")]
PathNotSignable(PathBuf),
#[error("error signing mach-o binary: {0}")]
MachOSigningError(AppleCodesignError),
#[error("error signing Apple bundle: {0}")]
AppleBundleSigningError(AppleCodesignError),
#[error("error running settings callback: {0}")]
SettingsCallback(anyhow::Error),
#[error("error running signtool: {0}")]
SigntoolError(anyhow::Error),
#[error("incompatible signing destination: {0}")]
IncompatibleSigningDestination(&'static str),
#[error("error when signing: {0}")]
GeneralSigning(String),
#[error("X.509 certificate handling error: {0}")]
X509Certificate(#[from] X509CertificateError),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SigningDestination {
File(PathBuf),
Directory(PathBuf),
Memory,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SigningDestinationCompatibility {
Compatible,
Incompatible(&'static str),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SignedOutput {
File(PathBuf),
Directory(PathBuf),
Memory(Vec<u8>),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SigningMethod {
InPlaceFile,
InPlaceDirectory,
NewFile,
NewDirectory,
Memory,
}
pub struct SigningMethods(Vec<SigningMethod>);
impl Deref for SigningMethods {
type Target = Vec<SigningMethod>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub enum SignableCandidate<'a> {
Path(Cow<'a, Path>),
Data(Cow<'a, [u8]>),
Forced(Signable),
}
impl<'a> From<&'a Path> for SignableCandidate<'a> {
fn from(p: &'a Path) -> Self {
Self::Path(Cow::Borrowed(p))
}
}
impl<'a> From<PathBuf> for SignableCandidate<'static> {
fn from(p: PathBuf) -> Self {
Self::Path(Cow::Owned(p))
}
}
impl<'a> From<&'a [u8]> for SignableCandidate<'a> {
fn from(b: &'a [u8]) -> Self {
Self::Data(Cow::Borrowed(b))
}
}
impl<'a> From<Vec<u8>> for SignableCandidate<'static> {
fn from(data: Vec<u8>) -> Self {
Self::Data(Cow::Owned(data))
}
}
impl<'a> TryFrom<FileData> for SignableCandidate<'static> {
type Error = anyhow::Error;
fn try_from(file: FileData) -> Result<Self, Self::Error> {
Ok(Self::Data(Cow::Owned(file.resolve()?)))
}
}
impl<'a> TryFrom<&FileData> for SignableCandidate<'static> {
type Error = anyhow::Error;
fn try_from(file: &FileData) -> Result<Self, Self::Error> {
Ok(Self::Data(Cow::Owned(file.resolve()?)))
}
}
impl<'a> TryFrom<FileEntry> for SignableCandidate<'static> {
type Error = anyhow::Error;
fn try_from(entry: FileEntry) -> Result<Self, Self::Error> {
SignableCandidate::try_from(entry.data)
}
}
impl<'a> TryFrom<&FileEntry> for SignableCandidate<'static> {
type Error = anyhow::Error;
fn try_from(entry: &FileEntry) -> Result<Self, Self::Error> {
SignableCandidate::try_from(&entry.data)
}
}
impl<'a> TryFrom<File> for SignableCandidate<'static> {
type Error = anyhow::Error;
fn try_from(file: File) -> Result<Self, Self::Error> {
SignableCandidate::try_from(file.entry.data)
}
}
impl<'a> TryFrom<&File> for SignableCandidate<'static> {
type Error = anyhow::Error;
fn try_from(file: &File) -> Result<Self, Self::Error> {
SignableCandidate::try_from(&file.entry.data)
}
}
#[derive(Clone, Debug)]
pub enum Signable {
WindowsFile(PathBuf),
WindowsData(Vec<u8>),
MachOFile(PathBuf, Vec<u8>),
MachOData(Vec<u8>),
AppleBundle(PathBuf),
}
impl Signable {
pub fn signing_methods(&self) -> SigningMethods {
SigningMethods(match self {
Self::WindowsFile(_) => {
vec![
SigningMethod::InPlaceFile,
SigningMethod::NewFile,
SigningMethod::Memory,
]
}
Self::WindowsData(_) => {
vec![
SigningMethod::NewFile,
SigningMethod::Memory,
]
}
Self::MachOFile(_, _) => {
vec![
SigningMethod::InPlaceFile,
SigningMethod::NewFile,
SigningMethod::Memory,
]
}
Self::MachOData(_) => {
vec![SigningMethod::NewFile, SigningMethod::Memory]
}
Self::AppleBundle(_) => {
vec![SigningMethod::InPlaceDirectory, SigningMethod::NewDirectory]
}
})
}
pub fn is_signable(&self) -> bool {
!self.signing_methods().is_empty()
}
pub fn source_file(&self) -> Option<&Path> {
match self {
Self::WindowsFile(p) => Some(p.as_path()),
Self::MachOFile(p, _) => Some(p.as_path()),
Self::WindowsData(_) | Self::MachOData(_) | Self::AppleBundle(_) => None,
}
}
pub fn source_directory(&self) -> Option<&Path> {
match self {
Self::AppleBundle(p) => Some(p.as_path()),
Self::WindowsFile(_)
| Self::WindowsData(_)
| Self::MachOFile(_, _)
| Self::MachOData(_) => None,
}
}
pub fn destination_compatibility(
&self,
destination: &SigningDestination,
) -> SigningDestinationCompatibility {
let methods = self.signing_methods();
match destination {
SigningDestination::Memory => {
if methods.iter().any(|x| *x == SigningMethod::Memory) {
SigningDestinationCompatibility::Compatible
} else {
SigningDestinationCompatibility::Incompatible("signing to memory not supported")
}
}
SigningDestination::File(dest_path) => {
let same_file = if let Some(source_path) = self.source_file() {
source_path == dest_path
} else {
false
};
let compatible = methods.iter().any(|x| match x {
SigningMethod::InPlaceFile => same_file,
SigningMethod::NewFile => true,
SigningMethod::InPlaceDirectory
| SigningMethod::NewDirectory
| SigningMethod::Memory => false,
});
if compatible {
SigningDestinationCompatibility::Compatible
} else if same_file {
SigningDestinationCompatibility::Incompatible(
"signing file in place not supported",
)
} else {
SigningDestinationCompatibility::Incompatible(
"signing to a new file not supported",
)
}
}
SigningDestination::Directory(dest_dir) => {
let same_dir = if let Some(source_dir) = self.source_directory() {
source_dir == dest_dir
} else {
false
};
let compatible = methods.iter().any(|x| match x {
SigningMethod::InPlaceDirectory => same_dir,
SigningMethod::NewDirectory => true,
SigningMethod::InPlaceFile | SigningMethod::NewFile | SigningMethod::Memory => {
false
}
});
if compatible {
SigningDestinationCompatibility::Compatible
} else if same_dir {
SigningDestinationCompatibility::Incompatible(
"signing directory in place not supported",
)
} else {
SigningDestinationCompatibility::Incompatible(
"signing to a new directory not supported",
)
}
}
}
}
}
#[derive(Debug)]
pub enum Signability {
Signable(Signable),
Unsignable,
UnsignableMachoError(AppleCodesignError),
PlatformUnsupported(&'static str),
}
pub fn path_signable(path: impl AsRef<Path>) -> Result<Signability, SigningError> {
let path = path.as_ref();
if path.is_file() {
match tugger_windows_codesign::is_file_signable(path) {
Ok(true) => {
return if cfg!(target_family = "windows") {
Ok(Signability::Signable(Signable::WindowsFile(
path.to_path_buf(),
)))
} else {
Ok(Signability::PlatformUnsupported(
"Windows signing requires running on Windows",
))
};
}
Ok(false) => {}
Err(e) => return Err(SigningError::SignableTestError(format!("{:?}", e))),
}
let data = std::fs::read(path)?;
if goblin::mach::Mach::parse(&data).is_ok() {
return Ok(match MachOSigner::new(&data) {
Ok(_) => Signability::Signable(Signable::MachOFile(path.to_path_buf(), data)),
Err(e) => Signability::UnsignableMachoError(e),
});
}
} else if path.is_dir() && tugger_apple_bundle::DirectoryBundle::new_from_path(path).is_ok() {
return Ok(Signability::Signable(Signable::AppleBundle(
path.to_path_buf(),
)));
}
Ok(Signability::Unsignable)
}
pub fn data_signable(data: &[u8]) -> Result<Signability, SigningError> {
if tugger_windows_codesign::is_signable_binary_header(data) {
return if cfg!(target_family = "windows") {
Ok(Signability::Signable(Signable::WindowsData(
data.as_ref().to_vec(),
)))
} else {
Ok(Signability::PlatformUnsupported(
"Windows signing requires running on Windows",
))
};
}
if goblin::mach::Mach::parse(&data).is_ok() {
return Ok(match MachOSigner::new(&data) {
Ok(_) => Signability::Signable(Signable::MachOData(data.to_vec())),
Err(e) => Signability::UnsignableMachoError(e),
});
}
Ok(Signability::Unsignable)
}
#[derive(Debug)]
pub enum SigningCertificate {
Memory(CapturedX509Certificate, InMemorySigningKeyPair),
PfxFile(
PathBuf,
String,
CapturedX509Certificate,
InMemorySigningKeyPair,
),
WindowsStoreAuto,
WindowsStoreSubject(SystemStore, String),
WindowsStoreSha1Thumbprint(SystemStore, String),
}
impl SigningCertificate {
pub fn from_pfx_file(path: impl AsRef<Path>, password: &str) -> Result<Self, SigningError> {
let data = std::fs::read(path.as_ref())?;
let (cert, key) = tugger_apple_codesign::parse_pfx_data(&data, password)
.map_err(|e| SigningError::PfxRead(format!("{:?}", e)))?;
Ok(Self::PfxFile(
path.as_ref().to_path_buf(),
password.to_string(),
cert,
key,
))
}
pub fn from_pfx_data(data: &[u8], password: &str) -> Result<Self, SigningError> {
let (cert, key) = tugger_apple_codesign::parse_pfx_data(data, password)
.map_err(|e| SigningError::PfxRead(format!("{:?}", e)))?;
Ok(Self::Memory(cert, key))
}
pub fn windows_store_with_subject(
store: &str,
subject: impl ToString,
) -> Result<Self, SigningError> {
let store =
SystemStore::try_from(store).map_err(SigningError::BadWindowsCertificateStore)?;
Ok(Self::WindowsStoreSubject(store, subject.to_string()))
}
pub fn windows_store_with_sha1_thumbprint(
store: &str,
thumbprint: impl ToString,
) -> Result<Self, SigningError> {
let store =
SystemStore::try_from(store).map_err(SigningError::BadWindowsCertificateStore)?;
Ok(Self::WindowsStoreSha1Thumbprint(
store,
thumbprint.to_string(),
))
}
pub fn to_windows_code_signing_certificate(
&self,
) -> Result<CodeSigningCertificate, SigningError> {
match self {
Self::WindowsStoreAuto => Ok(CodeSigningCertificate::Auto),
Self::WindowsStoreSha1Thumbprint(store, thumbprint) => Ok(
CodeSigningCertificate::Sha1Thumbprint(*store, thumbprint.clone()),
),
Self::WindowsStoreSubject(store, subject) => {
Ok(CodeSigningCertificate::SubjectName(*store, subject.clone()))
}
Self::PfxFile(path, password, _, _) => {
let mut f = FileBasedCodeSigningCertificate::new(path);
f.set_password(password);
Ok(CodeSigningCertificate::File(f))
}
Self::Memory(_, _) => {
unimplemented!();
}
}
}
}
pub type AppleSigningSettingsFn =
fn(&Signable, &mut tugger_apple_codesign::SigningSettings) -> Result<(), anyhow::Error>;
pub type WindowsSignerFn =
fn(&Signable, &mut tugger_windows_codesign::SigntoolSign) -> Result<(), anyhow::Error>;
pub struct Signer {
signing_certificate: SigningCertificate,
certificate_chain: Vec<CapturedX509Certificate>,
time_stamp_url: Option<Url>,
apple_signing_settings_fn: Option<Arc<AppleSigningSettingsFn>>,
windows_signer_fn: Option<Arc<WindowsSignerFn>>,
}
impl From<SigningCertificate> for Signer {
fn from(cert: SigningCertificate) -> Self {
Self::new(cert)
}
}
impl Signer {
pub fn new(signing_certificate: SigningCertificate) -> Self {
Self {
signing_certificate,
certificate_chain: vec![],
time_stamp_url: None,
apple_signing_settings_fn: None,
windows_signer_fn: None,
}
}
pub fn chain_certificate(&mut self, certificate: CapturedX509Certificate) {
self.certificate_chain.push(certificate);
}
pub fn chain_certificates_pem(&mut self, data: impl AsRef<[u8]>) -> Result<(), SigningError> {
let certs = CapturedX509Certificate::from_pem_multiple(data)?;
if certs.is_empty() {
Err(SigningError::NoCertificateData)
} else {
self.certificate_chain.extend(certs);
Ok(())
}
}
pub fn chain_certificates(
&mut self,
certificates: impl Iterator<Item = CapturedX509Certificate>,
) {
self.certificate_chain.extend(certificates);
}
#[cfg(target_os = "macos")]
pub fn chain_certificates_macos_keychain(&mut self) -> Result<(), SigningError> {
let cert: &CapturedX509Certificate = match &self.signing_certificate {
SigningCertificate::Memory(cert, _) => Ok(cert),
_ => Err(SigningError::CertificateResolutionFailure(
"can only operate on signing certificates loaded into memory".to_string(),
)),
}?;
if cert.subject_is_issuer() {
return Ok(());
}
let user_id = cert
.subject_name()
.find_first_attribute_string(bcder::Oid(
tugger_apple_codesign::OID_USER_ID.as_ref().into(),
))
.map_err(|e| {
SigningError::CertificateResolutionFailure(format!(
"failed to decode UID field in signing certificate: {:?}",
e
))
})?
.ok_or_else(|| {
SigningError::CertificateResolutionFailure(
"could not find UID in signing certificate".to_string(),
)
})?;
let domain = tugger_apple_codesign::KeychainDomain::User;
let certs =
tugger_apple_codesign::macos_keychain_find_certificate_chain(domain, None, &user_id)
.map_err(SigningError::MacOsCertificateChainResolveFailure)?;
if certs.is_empty() {
return Err(SigningError::CertificateResolutionFailure(
"issuing certificates not found in macOS keychain".to_string(),
));
}
if !certs[certs.len() - 1].subject_is_issuer() {
return Err(SigningError::CertificateResolutionFailure(
"unable to resolve entire signing certificate chain; root certificate not found"
.to_string(),
));
}
self.certificate_chain.extend(certs);
Ok(())
}
#[cfg(not(target_os = "macos"))]
#[allow(unused_mut)]
pub fn chain_certificates_macos_keychain(&mut self) -> Result<(), SigningError> {
Err(SigningError::MacOsKeychainNotSupported)
}
pub fn time_stamp_url(&mut self, url: impl IntoUrl) -> Result<(), SigningError> {
let url = url.into_url().map_err(SigningError::BadUrl)?;
self.time_stamp_url = Some(url);
Ok(())
}
pub fn apple_settings_callback(&mut self, cb: AppleSigningSettingsFn) {
self.apple_signing_settings_fn = Some(Arc::new(cb));
}
pub fn windows_settings_callback(&mut self, cb: WindowsSignerFn) {
self.windows_signer_fn = Some(Arc::new(cb));
}
pub fn resolve_signability(
&self,
candidate: &SignableCandidate,
) -> Result<Signability, SigningError> {
let signability = match candidate {
SignableCandidate::Path(path) => path_signable(path),
SignableCandidate::Data(data) => data_signable(data.as_ref()),
SignableCandidate::Forced(signable) => Ok(Signability::Signable(signable.clone())),
}?;
if matches!(
signability,
Signability::Signable(Signable::WindowsFile(_))
| Signability::Signable(Signable::WindowsData(_))
) && matches!(self.signing_certificate, SigningCertificate::Memory(_, _))
{
Ok(Signability::PlatformUnsupported(
"do not support PFX key re-export on Windows",
))
} else {
Ok(signability)
}
}
pub fn resolve_signer(
&self,
candidate: &SignableCandidate,
) -> Result<Option<SignableSigner<'_>>, SigningError> {
let signability = self.resolve_signability(candidate)?;
if let Signability::Signable(entity) = signability {
Ok(Some(SignableSigner::new(&self, entity)))
} else {
Ok(None)
}
}
}
pub struct SignableSigner<'a> {
signing_certificate: &'a SigningCertificate,
signable: Signable,
certificate_chain: Vec<CapturedX509Certificate>,
time_stamp_url: Option<Url>,
apple_signing_settings_fn: Option<Arc<AppleSigningSettingsFn>>,
windows_signer_fn: Option<Arc<WindowsSignerFn>>,
}
impl<'a> SignableSigner<'a> {
fn new(signer: &'a Signer, signable: Signable) -> Self {
let signing_certificate = &signer.signing_certificate;
let certificate_chain = signer.certificate_chain.clone();
let time_stamp_url = signer.time_stamp_url.clone();
Self {
signing_certificate,
signable,
certificate_chain,
time_stamp_url,
apple_signing_settings_fn: signer.apple_signing_settings_fn.clone(),
windows_signer_fn: signer.windows_signer_fn.clone(),
}
}
pub fn signable(&self) -> &Signable {
&self.signable
}
pub fn in_place_destination(&self) -> SigningDestination {
match &self.signable {
Signable::WindowsFile(path) => SigningDestination::File(path.clone()),
Signable::MachOFile(path, _) => SigningDestination::File(path.clone()),
Signable::AppleBundle(path) => SigningDestination::Directory(path.clone()),
Signable::WindowsData(_) | Signable::MachOData(_) => SigningDestination::Memory,
}
}
pub fn as_apple_signing_settings(
&self,
) -> Result<tugger_apple_codesign::SigningSettings<'_>, SigningError> {
let mut settings = tugger_apple_codesign::SigningSettings::default();
match &self.signing_certificate {
SigningCertificate::Memory(cert, key) => {
settings.set_signing_key(key, cert.clone());
}
SigningCertificate::PfxFile(_, _, cert, key) => {
settings.set_signing_key(key, cert.clone());
}
SigningCertificate::WindowsStoreSubject(_, _)
| SigningCertificate::WindowsStoreSha1Thumbprint(_, _)
| SigningCertificate::WindowsStoreAuto => {
return Err(SigningError::CertificateNotUsable("certificates in the Windows store are not supported for signing Apple primitives; try using a PFX file-based certificate instead".to_string()));
}
};
settings.chain_apple_certificates();
for cert in &self.certificate_chain {
settings.chain_certificate(cert.clone());
}
if let Some(url) = &self.time_stamp_url {
settings
.set_time_stamp_url(url.clone())
.expect("shouldn't have failed for already parsed URL");
} else {
settings
.set_time_stamp_url(APPLE_TIMESTAMP_URL)
.expect("shouldn't have failed for constant URL");
}
if let Some(cb) = &self.apple_signing_settings_fn {
cb(&self.signable, &mut settings).map_err(SigningError::SettingsCallback)?;
}
Ok(settings)
}
pub fn as_windows_signer(&self) -> Result<tugger_windows_codesign::SigntoolSign, SigningError> {
let cert = self
.signing_certificate
.to_windows_code_signing_certificate()?;
let mut signer = tugger_windows_codesign::SigntoolSign::new(cert);
if let Some(url) = &self.time_stamp_url {
signer.timestamp_server(tugger_windows_codesign::TimestampServer::Rfc3161(
url.to_string(),
"SHA256".to_string(),
));
}
signer.file_digest_algorithm("SHA256");
if let Some(cb) = &self.windows_signer_fn {
cb(&self.signable, &mut signer).map_err(SigningError::SettingsCallback)?;
}
Ok(signer)
}
pub fn destination_compatibility(
&self,
destination: &SigningDestination,
) -> SigningDestinationCompatibility {
self.signable.destination_compatibility(destination)
}
pub fn sign(
&self,
logger: &slog::Logger,
temp_dir: Option<&Path>,
destination: &SigningDestination,
) -> Result<SignedOutput, SigningError> {
if let SigningDestinationCompatibility::Incompatible(reason) =
self.destination_compatibility(destination)
{
return Err(SigningError::IncompatibleSigningDestination(reason));
}
let temp_dir = if self.requires_temporary_files(destination) {
let mut builder = tempfile::Builder::new();
builder.prefix("tugger-code-sign-");
Some(if let Some(temp_dir) = temp_dir {
builder.tempdir_in(temp_dir)
} else {
builder.tempdir()
}?)
} else {
None
};
match &self.signable {
Signable::WindowsData(data) => {
let mut signer = self.as_windows_signer()?;
let td = temp_dir.as_ref().unwrap().path();
let sign_path = td.join("sign_temp");
warn!(
logger,
"writing signable Windows data to temporary file to sign: {}",
sign_path.display()
);
std::fs::write(&sign_path, data)?;
signer.sign_file(&sign_path);
signer.run(logger).map_err(SigningError::SigntoolError)?;
match destination {
SigningDestination::Memory => {
warn!(logger, "signing success; reading signed file to memory");
Ok(SignedOutput::Memory(std::fs::read(&sign_path)?))
}
SigningDestination::File(dest_path) => {
if copy_file_needed(&dest_path, &sign_path)? {
warn!(
logger,
"signing success; copying signed file to {}",
dest_path.display()
);
std::fs::copy(&sign_path, dest_path)?;
} else {
warn!(logger, "signing success");
}
Ok(SignedOutput::File(dest_path.clone()))
}
SigningDestination::Directory(_) => {
panic!("illegal signing combination: SignableWindowsData -> :Directory");
}
}
}
Signable::WindowsFile(source_file) => {
let mut signer = self.as_windows_signer()?;
let sign_path = if let Some(temp_dir) = temp_dir.as_ref().map(|x| x.path()) {
let filename = source_file.file_name().ok_or_else(|| {
SigningError::GeneralSigning(format!(
"unable to resolve filename of {}",
source_file.display()
))
})?;
let sign_path = temp_dir.join(filename);
warn!(
logger,
"copying {} to {} to perform signing",
source_file.display(),
sign_path.display()
);
std::fs::copy(&source_file, &sign_path)?;
sign_path
} else {
warn!(logger, "signing {}", source_file.display());
source_file.clone()
};
signer.sign_file(&sign_path);
signer.run(logger).map_err(SigningError::SigntoolError)?;
match destination {
SigningDestination::Memory => {
warn!(logger, "signing success; reading signed file to memory");
Ok(SignedOutput::Memory(std::fs::read(&sign_path)?))
}
SigningDestination::File(dest_path) => {
if copy_file_needed(&sign_path, &dest_path)? {
warn!(
logger,
"signing success; copying signed file to {}",
dest_path.display()
);
std::fs::copy(&sign_path, dest_path)?;
} else {
warn!(logger, "signing success");
}
Ok(SignedOutput::File(dest_path.clone()))
}
SigningDestination::Directory(_) => {
panic!("illegal signing combination: SignableWindowsFile -> Directory");
}
}
}
Signable::MachOData(macho_data) => {
warn!(
logger,
"signing Mach-O binary from in-memory data of size {} bytes",
macho_data.len()
);
let settings = self.as_apple_signing_settings()?;
let signer = tugger_apple_codesign::MachOSigner::new(&macho_data)
.map_err(SigningError::MachOSigningError)?;
let mut dest = Vec::<u8>::with_capacity(macho_data.len() + 2_usize.pow(17));
signer
.write_signed_binary(&settings, &mut dest)
.map_err(SigningError::MachOSigningError)?;
match destination {
SigningDestination::Memory => {
warn!(logger, "Mach-O signing success; new size {}", dest.len());
Ok(SignedOutput::Memory(dest))
}
SigningDestination::File(dest_file) => {
warn!(
logger,
"Mach-O signing success; writing to {}",
dest_file.display()
);
std::fs::write(&dest_file, &dest)?;
Ok(SignedOutput::File(dest_file.clone()))
}
SigningDestination::Directory(_) => {
panic!("illegal signing combination: SignableMachOData -> Directory");
}
}
}
Signable::MachOFile(source_file, macho_data) => {
let settings = self.as_apple_signing_settings()?;
warn!(logger, "signing {}", source_file.display());
let signer = tugger_apple_codesign::MachOSigner::new(&macho_data)
.map_err(SigningError::MachOSigningError)?;
let mut dest = Vec::<u8>::with_capacity(macho_data.len() + 2_usize.pow(17));
signer
.write_signed_binary(&settings, &mut dest)
.map_err(SigningError::MachOSigningError)?;
match destination {
SigningDestination::Memory => {
warn!(logger, "Mach-O signing success; new size {}", dest.len());
Ok(SignedOutput::Memory(dest))
}
SigningDestination::File(dest_file) => {
warn!(
logger,
"Mach-O signing success; writing to {}",
dest_file.display()
);
std::fs::write(&dest_file, &dest)?;
Ok(SignedOutput::File(dest_file.clone()))
}
SigningDestination::Directory(_) => {
panic!("illegal signing combination: SignableMachOPath -> Directory");
}
}
}
Signable::AppleBundle(source_dir) => {
let settings = self.as_apple_signing_settings()?;
let dest_dir = match destination {
SigningDestination::Directory(d) => d,
_ => panic!("illegal signing combination: SignableAppleBundle -> !Directory"),
};
warn!(
logger,
"signing Apple bundle at {} to {}",
source_dir.display(),
dest_dir.display()
);
let signer = tugger_apple_codesign::BundleSigner::new_from_path(source_dir)
.map_err(SigningError::AppleBundleSigningError)?;
signer
.write_signed_bundle(logger, dest_dir, &settings)
.map_err(SigningError::AppleBundleSigningError)?;
Ok(SignedOutput::Directory(dest_dir.clone()))
}
}
}
pub fn requires_temporary_files(&self, destination: &SigningDestination) -> bool {
match &self.signable {
Signable::WindowsData(_) => true,
Signable::WindowsFile(source_file) => match destination {
SigningDestination::Memory => true,
SigningDestination::File(dest_file) => source_file != dest_file,
SigningDestination::Directory(_) => false,
},
Signable::MachOData(_) | Signable::MachOFile(_, _) => false,
Signable::AppleBundle(source_dir) => match destination {
SigningDestination::Directory(dest_dir) => source_dir != dest_dir,
SigningDestination::Memory | SigningDestination::File(_) => false,
},
}
}
}
fn copy_file_needed(source: &Path, dest: &Path) -> Result<bool, std::io::Error> {
if dest.exists() {
Ok(source.canonicalize()? != dest.canonicalize()?)
} else {
Ok(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
const APPLE_P12_DATA: &[u8] =
include_bytes!("../../tugger-apple-codesign/src/apple-codesign-testuser.p12");
const WINDOWS_PFX_DEFAULT_DATA: &[u8] = include_bytes!("windows-testuser-default.pfx");
const WINDOWS_PFX_NO_EXTRAS_DATA: &[u8] = include_bytes!("windows-testuser-no-extras.pfx");
#[test]
fn parse_apple_p12() {
SigningCertificate::from_pfx_data(APPLE_P12_DATA, "password123").unwrap();
}
#[test]
fn parse_windows_pfx() {
SigningCertificate::from_pfx_data(WINDOWS_PFX_DEFAULT_DATA, "password123").unwrap();
SigningCertificate::from_pfx_data(WINDOWS_PFX_NO_EXTRAS_DATA, "password123").unwrap();
}
#[test]
fn parse_windows_pfx_dynamic() {
let cert =
tugger_windows_codesign::create_self_signed_code_signing_certificate("test user")
.unwrap();
let pfx_data =
tugger_windows_codesign::certificate_to_pfx(&cert, "password", "name").unwrap();
SigningCertificate::from_pfx_data(&pfx_data, "password").unwrap();
}
#[test]
fn windows_store_with_subject() {
let cert = SigningCertificate::windows_store_with_subject("my", "test user").unwrap();
assert!(matches!(
cert,
SigningCertificate::WindowsStoreSubject(_, _)
));
}
}