1use {
66 apple_codesign::{cryptography::InMemoryPrivateKey, AppleCodesignError, MachOSigner},
67 cryptographic_message_syntax::CmsError,
68 log::warn,
69 reqwest::{IntoUrl, Url},
70 simple_file_manifest::{File, FileData, FileEntry},
71 std::{
72 borrow::Cow,
73 ops::Deref,
74 path::{Path, PathBuf},
75 sync::Arc,
76 },
77 thiserror::Error,
78 tugger_windows_codesign::{
79 CodeSigningCertificate, FileBasedCodeSigningCertificate, SystemStore,
80 },
81 x509_certificate::{CapturedX509Certificate, X509CertificateError},
82 yasna::ASN1Error,
83};
84
85pub const APPLE_TIMESTAMP_URL: &str = "http://timestamp.apple.com/ts01";
87
88#[derive(Debug, Error)]
90pub enum SigningError {
91 #[error("could not determine if path is signable: {0}")]
92 SignableTestError(String),
93
94 #[error("I/O error: {0}")]
96 Io(#[from] std::io::Error),
97
98 #[error("error reading ASN.1 data: {0}")]
99 Asn1(#[from] ASN1Error),
100
101 #[error("cryptography error: {0}")]
102 Cms(#[from] CmsError),
103
104 #[error("no certificate data was found")]
105 NoCertificateData,
106
107 #[error("incorrect decryption password")]
108 BadDecryptionPassword,
109
110 #[error("PFX reading error: {0}")]
111 PfxRead(String),
112
113 #[error("{0}")]
114 BadWindowsCertificateStore(String),
115
116 #[error("bad URL: {0}")]
117 BadUrl(reqwest::Error),
118
119 #[error("macOS keychain integration only supported on macOS")]
120 MacOsKeychainNotSupported,
121
122 #[error("failed to resolve signing certificate: {0}")]
123 CertificateResolutionFailure(String),
124
125 #[error("certificate not usable: {0}")]
126 CertificateNotUsable(String),
127
128 #[error("error resolving certificate chain: {0}")]
129 MacOsCertificateChainResolveFailure(AppleCodesignError),
130
131 #[error("path {0} is not signable")]
132 PathNotSignable(PathBuf),
133
134 #[error("error signing mach-o binary: {0}")]
135 MachOSigningError(AppleCodesignError),
136
137 #[error("error signing Apple bundle: {0}")]
138 AppleBundleSigningError(AppleCodesignError),
139
140 #[error("error running settings callback: {0}")]
141 SettingsCallback(anyhow::Error),
142
143 #[error("error running signtool: {0}")]
144 SigntoolError(anyhow::Error),
145
146 #[error("incompatible signing destination: {0}")]
147 IncompatibleSigningDestination(&'static str),
148
149 #[error("error when signing: {0}")]
150 GeneralSigning(String),
151
152 #[error("X.509 certificate handling error: {0}")]
153 X509Certificate(#[from] X509CertificateError),
154}
155
156#[derive(Clone, Debug, Eq, PartialEq)]
158pub enum SigningDestination {
159 File(PathBuf),
161
162 Directory(PathBuf),
164
165 Memory,
167}
168
169#[derive(Clone, Copy, Debug, Eq, PartialEq)]
171pub enum SigningDestinationCompatibility {
172 Compatible,
174
175 Incompatible(&'static str),
177}
178
179#[derive(Clone, Debug, Eq, PartialEq)]
181pub enum SignedOutput {
182 File(PathBuf),
184
185 Directory(PathBuf),
187
188 Memory(Vec<u8>),
190}
191
192#[derive(Clone, Copy, Debug, Eq, PartialEq)]
198pub enum SigningMethod {
199 InPlaceFile,
201
202 InPlaceDirectory,
204
205 NewFile,
207
208 NewDirectory,
210
211 Memory,
213}
214
215pub struct SigningMethods(Vec<SigningMethod>);
219
220impl Deref for SigningMethods {
221 type Target = Vec<SigningMethod>;
222
223 fn deref(&self) -> &Self::Target {
224 &self.0
225 }
226}
227
228pub enum SignableCandidate<'a> {
239 Path(Cow<'a, Path>),
243
244 Data(Cow<'a, [u8]>),
248
249 Forced(Signable),
251}
252
253impl<'a> From<&'a Path> for SignableCandidate<'a> {
254 fn from(p: &'a Path) -> Self {
255 Self::Path(Cow::Borrowed(p))
256 }
257}
258
259impl<'a> From<PathBuf> for SignableCandidate<'static> {
260 fn from(p: PathBuf) -> Self {
261 Self::Path(Cow::Owned(p))
262 }
263}
264
265impl<'a> From<&'a [u8]> for SignableCandidate<'a> {
266 fn from(b: &'a [u8]) -> Self {
267 Self::Data(Cow::Borrowed(b))
268 }
269}
270
271impl<'a> From<Vec<u8>> for SignableCandidate<'static> {
272 fn from(data: Vec<u8>) -> Self {
273 Self::Data(Cow::Owned(data))
274 }
275}
276
277impl<'a> TryFrom<FileData> for SignableCandidate<'static> {
278 type Error = anyhow::Error;
279
280 fn try_from(file: FileData) -> Result<Self, Self::Error> {
281 Ok(Self::Data(Cow::Owned(file.resolve_content()?)))
282 }
283}
284
285impl<'a> TryFrom<&FileData> for SignableCandidate<'static> {
286 type Error = anyhow::Error;
287
288 fn try_from(file: &FileData) -> Result<Self, Self::Error> {
289 Ok(Self::Data(Cow::Owned(file.resolve_content()?)))
290 }
291}
292
293impl<'a> TryFrom<FileEntry> for SignableCandidate<'static> {
294 type Error = anyhow::Error;
295
296 fn try_from(entry: FileEntry) -> Result<Self, Self::Error> {
297 SignableCandidate::try_from(entry.file_data())
298 }
299}
300
301impl<'a> TryFrom<&FileEntry> for SignableCandidate<'static> {
302 type Error = anyhow::Error;
303
304 fn try_from(entry: &FileEntry) -> Result<Self, Self::Error> {
305 SignableCandidate::try_from(entry.file_data())
306 }
307}
308
309impl<'a> TryFrom<File> for SignableCandidate<'static> {
310 type Error = anyhow::Error;
311
312 fn try_from(file: File) -> Result<Self, Self::Error> {
313 SignableCandidate::try_from(file.entry().file_data())
314 }
315}
316
317impl<'a> TryFrom<&File> for SignableCandidate<'static> {
318 type Error = anyhow::Error;
319
320 fn try_from(file: &File) -> Result<Self, Self::Error> {
321 SignableCandidate::try_from(file.entry().file_data())
322 }
323}
324
325#[derive(Clone, Debug)]
327pub enum Signable {
328 WindowsFile(PathBuf),
330
331 WindowsData(Vec<u8>),
335
336 MachOFile(PathBuf, Vec<u8>),
341
342 MachOData(Vec<u8>),
344
345 AppleBundle(PathBuf),
347}
348
349impl Signable {
350 pub fn signing_methods(&self) -> SigningMethods {
352 SigningMethods(match self {
353 Self::WindowsFile(_) => {
354 vec![
355 SigningMethod::InPlaceFile,
357 SigningMethod::NewFile,
359 SigningMethod::Memory,
361 ]
362 }
363 Self::WindowsData(_) => {
364 vec![
365 SigningMethod::NewFile,
367 SigningMethod::Memory,
369 ]
370 }
371 Self::MachOFile(_, _) => {
372 vec![
374 SigningMethod::InPlaceFile,
375 SigningMethod::NewFile,
376 SigningMethod::Memory,
377 ]
378 }
379 Self::MachOData(_) => {
380 vec![SigningMethod::NewFile, SigningMethod::Memory]
382 }
383 Self::AppleBundle(_) => {
384 vec![SigningMethod::InPlaceDirectory, SigningMethod::NewDirectory]
386 }
387 })
388 }
389
390 pub fn is_signable(&self) -> bool {
392 !self.signing_methods().is_empty()
393 }
394
395 pub fn source_file(&self) -> Option<&Path> {
397 match self {
398 Self::WindowsFile(p) => Some(p.as_path()),
399 Self::MachOFile(p, _) => Some(p.as_path()),
400 Self::WindowsData(_) | Self::MachOData(_) | Self::AppleBundle(_) => None,
401 }
402 }
403
404 pub fn source_directory(&self) -> Option<&Path> {
406 match self {
407 Self::AppleBundle(p) => Some(p.as_path()),
408 Self::WindowsFile(_)
409 | Self::WindowsData(_)
410 | Self::MachOFile(_, _)
411 | Self::MachOData(_) => None,
412 }
413 }
414
415 pub fn destination_compatibility(
417 &self,
418 destination: &SigningDestination,
419 ) -> SigningDestinationCompatibility {
420 let methods = self.signing_methods();
421
422 match destination {
423 SigningDestination::Memory => {
424 if methods.iter().any(|x| *x == SigningMethod::Memory) {
425 SigningDestinationCompatibility::Compatible
426 } else {
427 SigningDestinationCompatibility::Incompatible("signing to memory not supported")
428 }
429 }
430 SigningDestination::File(dest_path) => {
431 let same_file = if let Some(source_path) = self.source_file() {
432 source_path == dest_path
433 } else {
434 false
435 };
436
437 let compatible = methods.iter().any(|x| match x {
438 SigningMethod::InPlaceFile => same_file,
439 SigningMethod::NewFile => true,
440 SigningMethod::InPlaceDirectory
441 | SigningMethod::NewDirectory
442 | SigningMethod::Memory => false,
443 });
444
445 if compatible {
446 SigningDestinationCompatibility::Compatible
447 } else if same_file {
448 SigningDestinationCompatibility::Incompatible(
449 "signing file in place not supported",
450 )
451 } else {
452 SigningDestinationCompatibility::Incompatible(
453 "signing to a new file not supported",
454 )
455 }
456 }
457 SigningDestination::Directory(dest_dir) => {
458 let same_dir = if let Some(source_dir) = self.source_directory() {
459 source_dir == dest_dir
460 } else {
461 false
462 };
463
464 let compatible = methods.iter().any(|x| match x {
465 SigningMethod::InPlaceDirectory => same_dir,
466 SigningMethod::NewDirectory => true,
467 SigningMethod::InPlaceFile | SigningMethod::NewFile | SigningMethod::Memory => {
468 false
469 }
470 });
471
472 if compatible {
473 SigningDestinationCompatibility::Compatible
474 } else if same_dir {
475 SigningDestinationCompatibility::Incompatible(
476 "signing directory in place not supported",
477 )
478 } else {
479 SigningDestinationCompatibility::Incompatible(
480 "signing to a new directory not supported",
481 )
482 }
483 }
484 }
485 }
486}
487
488#[derive(Debug)]
490pub enum Signability {
491 Signable(Signable),
493
494 Unsignable,
496
497 UnsignableMachoError(AppleCodesignError),
499
500 PlatformUnsupported(&'static str),
503}
504
505pub fn path_signable(path: impl AsRef<Path>) -> Result<Signability, SigningError> {
512 let path = path.as_ref();
513
514 if path.is_file() {
515 match tugger_windows_codesign::is_file_signable(path) {
516 Ok(true) => {
517 return if cfg!(target_family = "windows") {
520 Ok(Signability::Signable(Signable::WindowsFile(
521 path.to_path_buf(),
522 )))
523 } else {
524 Ok(Signability::PlatformUnsupported(
525 "Windows signing requires running on Windows",
526 ))
527 };
528 }
529 Ok(false) => {}
530 Err(e) => return Err(SigningError::SignableTestError(format!("{:?}", e))),
531 }
532
533 let data = std::fs::read(path)?;
534
535 if goblin::mach::Mach::parse(&data).is_ok() {
536 return Ok(match MachOSigner::new(&data) {
538 Ok(_) => Signability::Signable(Signable::MachOFile(path.to_path_buf(), data)),
539 Err(e) => Signability::UnsignableMachoError(e),
540 });
541 }
542 } else if path.is_dir() && apple_bundles::DirectoryBundle::new_from_path(path).is_ok() {
543 return Ok(Signability::Signable(Signable::AppleBundle(
544 path.to_path_buf(),
545 )));
546 }
547
548 Ok(Signability::Unsignable)
549}
550
551pub fn data_signable(data: &[u8]) -> Result<Signability, SigningError> {
553 if tugger_windows_codesign::is_signable_binary_header(data) {
554 return if cfg!(target_family = "windows") {
557 Ok(Signability::Signable(Signable::WindowsData(
558 data.as_ref().to_vec(),
559 )))
560 } else {
561 Ok(Signability::PlatformUnsupported(
562 "Windows signing requires running on Windows",
563 ))
564 };
565 }
566
567 if goblin::mach::Mach::parse(data).is_ok() {
568 return Ok(match MachOSigner::new(data) {
570 Ok(_) => Signability::Signable(Signable::MachOData(data.to_vec())),
571 Err(e) => Signability::UnsignableMachoError(e),
572 });
573 }
574
575 Ok(Signability::Unsignable)
576}
577
578#[derive(Debug)]
580pub enum SigningCertificate {
581 Memory(CapturedX509Certificate, InMemoryPrivateKey),
585
586 PfxFile(PathBuf, String, CapturedX509Certificate, InMemoryPrivateKey),
590
591 WindowsStoreAuto,
593
594 WindowsStoreSubject(SystemStore, String),
603
604 WindowsStoreSha1Thumbprint(SystemStore, String),
612}
613
614impl SigningCertificate {
615 pub fn from_pfx_file(path: impl AsRef<Path>, password: &str) -> Result<Self, SigningError> {
621 let data = std::fs::read(path.as_ref())?;
622
623 let (cert, key) = apple_codesign::cryptography::parse_pfx_data(&data, password)
625 .map_err(|e| SigningError::PfxRead(format!("{:?}", e)))?;
626
627 Ok(Self::PfxFile(
628 path.as_ref().to_path_buf(),
629 password.to_string(),
630 cert,
631 key,
632 ))
633 }
634
635 pub fn from_pfx_data(data: &[u8], password: &str) -> Result<Self, SigningError> {
645 let (cert, key) = apple_codesign::cryptography::parse_pfx_data(data, password)
646 .map_err(|e| SigningError::PfxRead(format!("{:?}", e)))?;
647
648 Ok(Self::Memory(cert, key))
649 }
650
651 pub fn windows_store_with_subject(
660 store: &str,
661 subject: impl ToString,
662 ) -> Result<Self, SigningError> {
663 let store =
664 SystemStore::try_from(store).map_err(SigningError::BadWindowsCertificateStore)?;
665
666 Ok(Self::WindowsStoreSubject(store, subject.to_string()))
667 }
668
669 pub fn windows_store_with_sha1_thumbprint(
678 store: &str,
679 thumbprint: impl ToString,
680 ) -> Result<Self, SigningError> {
681 let store =
682 SystemStore::try_from(store).map_err(SigningError::BadWindowsCertificateStore)?;
683
684 Ok(Self::WindowsStoreSha1Thumbprint(
685 store,
686 thumbprint.to_string(),
687 ))
688 }
689
690 pub fn to_windows_code_signing_certificate(
692 &self,
693 ) -> Result<CodeSigningCertificate, SigningError> {
694 match self {
695 Self::WindowsStoreAuto => Ok(CodeSigningCertificate::Auto),
696 Self::WindowsStoreSha1Thumbprint(store, thumbprint) => Ok(
697 CodeSigningCertificate::Sha1Thumbprint(*store, thumbprint.clone()),
698 ),
699 Self::WindowsStoreSubject(store, subject) => {
700 Ok(CodeSigningCertificate::SubjectName(*store, subject.clone()))
701 }
702 Self::PfxFile(path, password, _, _) => {
703 let mut f = FileBasedCodeSigningCertificate::new(path);
704 f.set_password(password);
705
706 Ok(CodeSigningCertificate::File(f))
707 }
708 Self::Memory(_, _) => {
709 unimplemented!();
712 }
713 }
714 }
715}
716
717pub type AppleSigningSettingsFn =
720 fn(&Signable, &mut apple_codesign::SigningSettings) -> Result<(), anyhow::Error>;
721
722pub type WindowsSignerFn =
725 fn(&Signable, &mut tugger_windows_codesign::SigntoolSign) -> Result<(), anyhow::Error>;
726
727pub struct Signer {
732 signing_certificate: SigningCertificate,
734
735 certificate_chain: Vec<CapturedX509Certificate>,
740
741 time_stamp_url: Option<Url>,
743
744 apple_signing_settings_fn: Option<Arc<AppleSigningSettingsFn>>,
747
748 windows_signer_fn: Option<Arc<WindowsSignerFn>>,
751}
752
753impl From<SigningCertificate> for Signer {
754 fn from(cert: SigningCertificate) -> Self {
755 Self::new(cert)
756 }
757}
758
759impl Signer {
760 pub fn new(signing_certificate: SigningCertificate) -> Self {
762 Self {
763 signing_certificate,
764 certificate_chain: vec![],
765 time_stamp_url: None,
766 apple_signing_settings_fn: None,
767 windows_signer_fn: None,
768 }
769 }
770
771 pub fn chain_certificate(&mut self, certificate: CapturedX509Certificate) {
780 self.certificate_chain.push(certificate);
781 }
782
783 pub fn chain_certificates_pem(&mut self, data: impl AsRef<[u8]>) -> Result<(), SigningError> {
790 let certs = CapturedX509Certificate::from_pem_multiple(data)?;
791
792 if certs.is_empty() {
793 Err(SigningError::NoCertificateData)
794 } else {
795 self.certificate_chain.extend(certs);
796 Ok(())
797 }
798 }
799
800 pub fn chain_certificates(
804 &mut self,
805 certificates: impl Iterator<Item = CapturedX509Certificate>,
806 ) {
807 self.certificate_chain.extend(certificates);
808 }
809
810 #[cfg(target_os = "macos")]
821 pub fn chain_certificates_macos_keychain(&mut self) -> Result<(), SigningError> {
822 let cert: &CapturedX509Certificate = match &self.signing_certificate {
823 SigningCertificate::Memory(cert, _) => Ok(cert),
824 _ => Err(SigningError::CertificateResolutionFailure(
825 "can only operate on signing certificates loaded into memory".to_string(),
826 )),
827 }?;
828
829 if cert.subject_is_issuer() {
830 return Ok(());
831 }
832
833 let user_id = cert
834 .subject_name()
835 .find_first_attribute_string(bcder::Oid(apple_codesign::OID_USER_ID.as_ref().into()))
836 .map_err(|e| {
837 SigningError::CertificateResolutionFailure(format!(
838 "failed to decode UID field in signing certificate: {:?}",
839 e
840 ))
841 })?
842 .ok_or_else(|| {
843 SigningError::CertificateResolutionFailure(
844 "could not find UID in signing certificate".to_string(),
845 )
846 })?;
847
848 let domain = apple_codesign::KeychainDomain::User;
849
850 let certs = apple_codesign::macos_keychain_find_certificate_chain(domain, None, &user_id)
851 .map_err(SigningError::MacOsCertificateChainResolveFailure)?;
852
853 if certs.is_empty() {
854 return Err(SigningError::CertificateResolutionFailure(
855 "issuing certificates not found in macOS keychain".to_string(),
856 ));
857 }
858
859 if !certs[certs.len() - 1].subject_is_issuer() {
860 return Err(SigningError::CertificateResolutionFailure(
861 "unable to resolve entire signing certificate chain; root certificate not found"
862 .to_string(),
863 ));
864 }
865
866 self.certificate_chain.extend(certs);
867 Ok(())
868 }
869
870 #[cfg(not(target_os = "macos"))]
881 #[allow(unused_mut)]
882 pub fn chain_certificates_macos_keychain(&mut self) -> Result<(), SigningError> {
883 Err(SigningError::MacOsKeychainNotSupported)
884 }
885
886 pub fn time_stamp_url(&mut self, url: impl IntoUrl) -> Result<(), SigningError> {
892 let url = url.into_url().map_err(SigningError::BadUrl)?;
893 self.time_stamp_url = Some(url);
894 Ok(())
895 }
896
897 pub fn apple_settings_callback(&mut self, cb: AppleSigningSettingsFn) {
899 self.apple_signing_settings_fn = Some(Arc::new(cb));
900 }
901
902 pub fn windows_settings_callback(&mut self, cb: WindowsSignerFn) {
904 self.windows_signer_fn = Some(Arc::new(cb));
905 }
906
907 pub fn resolve_signability(
909 &self,
910 candidate: &SignableCandidate,
911 ) -> Result<Signability, SigningError> {
912 let signability = match candidate {
913 SignableCandidate::Path(path) => path_signable(path),
914 SignableCandidate::Data(data) => data_signable(data.as_ref()),
915 SignableCandidate::Forced(signable) => Ok(Signability::Signable(signable.clone())),
916 }?;
917
918 if matches!(
920 signability,
921 Signability::Signable(Signable::WindowsFile(_))
922 | Signability::Signable(Signable::WindowsData(_))
923 ) && matches!(self.signing_certificate, SigningCertificate::Memory(_, _))
924 {
925 Ok(Signability::PlatformUnsupported(
926 "do not support PFX key re-export on Windows",
927 ))
928 } else {
929 Ok(signability)
930 }
931 }
932
933 pub fn resolve_signer(
941 &self,
942 candidate: &SignableCandidate,
943 ) -> Result<Option<SignableSigner<'_>>, SigningError> {
944 let signability = self.resolve_signability(candidate)?;
945
946 if let Signability::Signable(entity) = signability {
947 Ok(Some(SignableSigner::new(self, entity)))
948 } else {
949 Ok(None)
950 }
951 }
952}
953
954pub struct SignableSigner<'a> {
960 signing_certificate: &'a SigningCertificate,
962
963 signable: Signable,
965
966 certificate_chain: Vec<CapturedX509Certificate>,
971
972 time_stamp_url: Option<Url>,
974
975 apple_signing_settings_fn: Option<Arc<AppleSigningSettingsFn>>,
978
979 windows_signer_fn: Option<Arc<WindowsSignerFn>>,
982}
983
984impl<'a> SignableSigner<'a> {
985 fn new(signer: &'a Signer, signable: Signable) -> Self {
986 let signing_certificate = &signer.signing_certificate;
987 let certificate_chain = signer.certificate_chain.clone();
988 let time_stamp_url = signer.time_stamp_url.clone();
989
990 Self {
991 signing_certificate,
992 signable,
993 certificate_chain,
994 time_stamp_url,
995 apple_signing_settings_fn: signer.apple_signing_settings_fn.clone(),
996 windows_signer_fn: signer.windows_signer_fn.clone(),
997 }
998 }
999
1000 pub fn signable(&self) -> &Signable {
1002 &self.signable
1003 }
1004
1005 pub fn in_place_destination(&self) -> SigningDestination {
1007 match &self.signable {
1008 Signable::WindowsFile(path) => SigningDestination::File(path.clone()),
1009 Signable::MachOFile(path, _) => SigningDestination::File(path.clone()),
1010 Signable::AppleBundle(path) => SigningDestination::Directory(path.clone()),
1011 Signable::WindowsData(_) | Signable::MachOData(_) => SigningDestination::Memory,
1012 }
1013 }
1014
1015 pub fn as_apple_signing_settings(
1017 &self,
1018 ) -> Result<apple_codesign::SigningSettings<'_>, SigningError> {
1019 let mut settings = apple_codesign::SigningSettings::default();
1020
1021 match &self.signing_certificate {
1022 SigningCertificate::Memory(cert, key) => {
1023 settings.set_signing_key(key, cert.clone());
1024 }
1025 SigningCertificate::PfxFile(_, _, cert, key) => {
1026 settings.set_signing_key(key, cert.clone());
1027 }
1028 SigningCertificate::WindowsStoreSubject(_, _)
1029 | SigningCertificate::WindowsStoreSha1Thumbprint(_, _)
1030 | SigningCertificate::WindowsStoreAuto => {
1031 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()));
1032 }
1033 };
1034
1035 settings.chain_apple_certificates();
1037
1038 for cert in &self.certificate_chain {
1039 settings.chain_certificate(cert.clone());
1040 }
1041
1042 if let Some(url) = &self.time_stamp_url {
1043 settings
1044 .set_time_stamp_url(url.clone())
1045 .expect("shouldn't have failed for already parsed URL");
1046 } else {
1047 settings
1048 .set_time_stamp_url(APPLE_TIMESTAMP_URL)
1049 .expect("shouldn't have failed for constant URL");
1050 }
1051
1052 if let Some(cb) = &self.apple_signing_settings_fn {
1053 cb(&self.signable, &mut settings).map_err(SigningError::SettingsCallback)?;
1054 }
1055
1056 Ok(settings)
1057 }
1058
1059 pub fn as_windows_signer(&self) -> Result<tugger_windows_codesign::SigntoolSign, SigningError> {
1061 let cert = self
1062 .signing_certificate
1063 .to_windows_code_signing_certificate()?;
1064
1065 let mut signer = tugger_windows_codesign::SigntoolSign::new(cert);
1066
1067 if let Some(url) = &self.time_stamp_url {
1068 signer.timestamp_server(tugger_windows_codesign::TimestampServer::Rfc3161(
1069 url.to_string(),
1070 "SHA256".to_string(),
1071 ));
1072 }
1073
1074 signer.file_digest_algorithm("SHA256");
1075
1076 if let Some(cb) = &self.windows_signer_fn {
1077 cb(&self.signable, &mut signer).map_err(SigningError::SettingsCallback)?;
1078 }
1079
1080 Ok(signer)
1081 }
1082
1083 pub fn destination_compatibility(
1087 &self,
1088 destination: &SigningDestination,
1089 ) -> SigningDestinationCompatibility {
1090 self.signable.destination_compatibility(destination)
1091 }
1092
1093 pub fn sign(
1104 &self,
1105
1106 temp_dir: Option<&Path>,
1107 destination: &SigningDestination,
1108 ) -> Result<SignedOutput, SigningError> {
1109 if let SigningDestinationCompatibility::Incompatible(reason) =
1110 self.destination_compatibility(destination)
1111 {
1112 return Err(SigningError::IncompatibleSigningDestination(reason));
1113 }
1114
1115 let temp_dir = if self.requires_temporary_files(destination) {
1116 let mut builder = tempfile::Builder::new();
1117 builder.prefix("tugger-code-sign-");
1118
1119 Some(if let Some(temp_dir) = temp_dir {
1120 builder.tempdir_in(temp_dir)
1121 } else {
1122 builder.tempdir()
1123 }?)
1124 } else {
1125 None
1126 };
1127
1128 match &self.signable {
1129 Signable::WindowsData(data) => {
1130 let mut signer = self.as_windows_signer()?;
1131
1132 let td = temp_dir.as_ref().unwrap().path();
1137
1138 let sign_path = td.join("sign_temp");
1139 warn!(
1140 "writing signable Windows data to temporary file to sign: {}",
1141 sign_path.display()
1142 );
1143 std::fs::write(&sign_path, data)?;
1144
1145 signer.sign_file(&sign_path);
1146 signer.run().map_err(SigningError::SigntoolError)?;
1147
1148 match destination {
1149 SigningDestination::Memory => {
1150 warn!("signing success; reading signed file to memory");
1151 Ok(SignedOutput::Memory(std::fs::read(&sign_path)?))
1152 }
1153 SigningDestination::File(dest_path) => {
1154 if copy_file_needed(dest_path, &sign_path)? {
1155 warn!(
1156 "signing success; copying signed file to {}",
1157 dest_path.display()
1158 );
1159 std::fs::copy(&sign_path, dest_path)?;
1160 } else {
1161 warn!("signing success");
1162 }
1163
1164 Ok(SignedOutput::File(dest_path.clone()))
1165 }
1166 SigningDestination::Directory(_) => {
1167 panic!("illegal signing combination: SignableWindowsData -> :Directory");
1168 }
1169 }
1170 }
1171 Signable::WindowsFile(source_file) => {
1172 let mut signer = self.as_windows_signer()?;
1173
1174 let sign_path = if let Some(temp_dir) = temp_dir.as_ref().map(|x| x.path()) {
1177 let filename = source_file.file_name().ok_or_else(|| {
1178 SigningError::GeneralSigning(format!(
1179 "unable to resolve filename of {}",
1180 source_file.display()
1181 ))
1182 })?;
1183
1184 let sign_path = temp_dir.join(filename);
1185 warn!(
1186 "copying {} to {} to perform signing",
1187 source_file.display(),
1188 sign_path.display()
1189 );
1190 std::fs::copy(source_file, &sign_path)?;
1191
1192 sign_path
1193 } else {
1194 warn!("signing {}", source_file.display());
1195 source_file.clone()
1196 };
1197
1198 signer.sign_file(&sign_path);
1199 signer.run().map_err(SigningError::SigntoolError)?;
1200
1201 match destination {
1202 SigningDestination::Memory => {
1203 warn!("signing success; reading signed file to memory");
1204 Ok(SignedOutput::Memory(std::fs::read(&sign_path)?))
1205 }
1206 SigningDestination::File(dest_path) => {
1207 if copy_file_needed(&sign_path, dest_path)? {
1208 warn!(
1209 "signing success; copying signed file to {}",
1210 dest_path.display()
1211 );
1212 std::fs::copy(&sign_path, dest_path)?;
1213 } else {
1214 warn!("signing success");
1215 }
1216
1217 Ok(SignedOutput::File(dest_path.clone()))
1218 }
1219 SigningDestination::Directory(_) => {
1220 panic!("illegal signing combination: SignableWindowsFile -> Directory");
1221 }
1222 }
1223 }
1224 Signable::MachOData(macho_data) => {
1225 warn!(
1226 "signing Mach-O binary from in-memory data of size {} bytes",
1227 macho_data.len()
1228 );
1229 let settings = self.as_apple_signing_settings()?;
1230
1231 let signer = apple_codesign::MachOSigner::new(macho_data)
1232 .map_err(SigningError::MachOSigningError)?;
1233
1234 let mut dest = Vec::<u8>::with_capacity(macho_data.len() + 2_usize.pow(17));
1235 signer
1236 .write_signed_binary(&settings, &mut dest)
1237 .map_err(SigningError::MachOSigningError)?;
1238
1239 match destination {
1240 SigningDestination::Memory => {
1241 warn!("Mach-O signing success; new size {}", dest.len());
1242 Ok(SignedOutput::Memory(dest))
1243 }
1244 SigningDestination::File(dest_file) => {
1245 warn!("Mach-O signing success; writing to {}", dest_file.display());
1246 std::fs::write(dest_file, &dest)?;
1247 Ok(SignedOutput::File(dest_file.clone()))
1248 }
1249 SigningDestination::Directory(_) => {
1250 panic!("illegal signing combination: SignableMachOData -> Directory");
1251 }
1252 }
1253 }
1254 Signable::MachOFile(source_file, macho_data) => {
1255 let settings = self.as_apple_signing_settings()?;
1256
1257 warn!("signing {}", source_file.display());
1258
1259 let signer = apple_codesign::MachOSigner::new(macho_data)
1260 .map_err(SigningError::MachOSigningError)?;
1261
1262 let mut dest = Vec::<u8>::with_capacity(macho_data.len() + 2_usize.pow(17));
1263 signer
1264 .write_signed_binary(&settings, &mut dest)
1265 .map_err(SigningError::MachOSigningError)?;
1266
1267 match destination {
1268 SigningDestination::Memory => {
1269 warn!("Mach-O signing success; new size {}", dest.len());
1270 Ok(SignedOutput::Memory(dest))
1271 }
1272 SigningDestination::File(dest_file) => {
1273 warn!("Mach-O signing success; writing to {}", dest_file.display());
1274 std::fs::write(dest_file, &dest)?;
1275 Ok(SignedOutput::File(dest_file.clone()))
1276 }
1277 SigningDestination::Directory(_) => {
1278 panic!("illegal signing combination: SignableMachOPath -> Directory");
1279 }
1280 }
1281 }
1282 Signable::AppleBundle(source_dir) => {
1283 let settings = self.as_apple_signing_settings()?;
1284
1285 let dest_dir = match destination {
1287 SigningDestination::Directory(d) => d,
1288 _ => panic!("illegal signing combination: SignableAppleBundle -> !Directory"),
1289 };
1290
1291 warn!(
1292 "signing Apple bundle at {} to {}",
1293 source_dir.display(),
1294 dest_dir.display()
1295 );
1296
1297 let signer = apple_codesign::BundleSigner::new_from_path(source_dir)
1298 .map_err(SigningError::AppleBundleSigningError)?;
1299
1300 signer
1301 .write_signed_bundle(dest_dir, &settings)
1302 .map_err(SigningError::AppleBundleSigningError)?;
1303
1304 Ok(SignedOutput::Directory(dest_dir.clone()))
1305 }
1306 }
1307 }
1308
1309 pub fn requires_temporary_files(&self, destination: &SigningDestination) -> bool {
1321 match &self.signable {
1322 Signable::WindowsData(_) => true,
1324 Signable::WindowsFile(source_file) => match destination {
1325 SigningDestination::Memory => true,
1327 SigningDestination::File(dest_file) => source_file != dest_file,
1330 SigningDestination::Directory(_) => false,
1332 },
1333 Signable::MachOData(_) | Signable::MachOFile(_, _) => false,
1335 Signable::AppleBundle(source_dir) => match destination {
1339 SigningDestination::Directory(dest_dir) => source_dir != dest_dir,
1340 SigningDestination::Memory | SigningDestination::File(_) => false,
1341 },
1342 }
1343 }
1344}
1345
1346fn copy_file_needed(source: &Path, dest: &Path) -> Result<bool, std::io::Error> {
1352 if dest.exists() {
1353 Ok(source.canonicalize()? != dest.canonicalize()?)
1354 } else {
1355 Ok(true)
1356 }
1357}
1358
1359#[cfg(test)]
1360mod tests {
1361 use super::*;
1362
1363 const APPLE_P12_DATA: &[u8] = include_bytes!("apple-codesign-testuser.p12");
1364
1365 const WINDOWS_PFX_DEFAULT_DATA: &[u8] = include_bytes!("windows-testuser-default.pfx");
1366 const WINDOWS_PFX_NO_EXTRAS_DATA: &[u8] = include_bytes!("windows-testuser-no-extras.pfx");
1367
1368 #[test]
1369 fn parse_apple_p12() {
1370 SigningCertificate::from_pfx_data(APPLE_P12_DATA, "password123").unwrap();
1371 }
1372
1373 #[test]
1374 fn parse_windows_pfx() {
1375 SigningCertificate::from_pfx_data(WINDOWS_PFX_DEFAULT_DATA, "password123").unwrap();
1376 SigningCertificate::from_pfx_data(WINDOWS_PFX_NO_EXTRAS_DATA, "password123").unwrap();
1377 }
1378
1379 #[test]
1380 fn parse_windows_pfx_dynamic() {
1381 let cert =
1382 tugger_windows_codesign::create_self_signed_code_signing_certificate("test user")
1383 .unwrap();
1384 let pfx_data =
1385 tugger_windows_codesign::certificate_to_pfx(&cert, "password", "name").unwrap();
1386
1387 SigningCertificate::from_pfx_data(&pfx_data, "password").unwrap();
1388 }
1389
1390 #[test]
1391 fn windows_store_with_subject() {
1392 let cert = SigningCertificate::windows_store_with_subject("my", "test user").unwrap();
1393 assert!(matches!(
1394 cert,
1395 SigningCertificate::WindowsStoreSubject(_, _)
1396 ));
1397 }
1398}