tugger_code_signing/
lib.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//! Cross-platform interface for code signing.
6//!
7//! This crate implements functionality for performing code signing in
8//! a platform-agnostic manner. It attempts to abstract over platform
9//! differences so users don't care what platform they are running on or
10//! what type of entity they are signing. It achieves varying levels
11//! of success, depending on limitations of underlying crates.
12//!
13//! # General Workflow
14//!
15//! [SigningCertificate] represents a code signing certificate (logically a
16//! private key + a public X.509 certificate). Instances are constructed
17//! with a reference to a code signing certificate in one of the enumerated
18//! supported locations.
19//!
20//! [SigningCertificate] are converted into [Signer], which is a slightly
21//! broader scoped entity. [Signer] holds additional attributes beyond the
22//! [SigningCertificate], such as a list of issuing [CapturedX509Certificate]
23//! constituting the certificate chain and a Time-Stamp Protocol server URL to
24//! use.
25//!
26//! [SignableCandidate] represents the different potential data types that can
27//! be signed. e.g. a filesystem path or slice of data.
28//!
29//! [Signer] exposes a concrete test for whether a [SignableCandidate] is
30//! signable, via [Signer::resolve_signability]. The test relies on heuristics
31//! in the supported signing *backends* (e.g. [apple_codesign] and
32//! [tugger_windows_codesign]) as well as [Signer] specific state to determine
33//! if an entity is signable. False positives and negatives are possible:
34//! please report bugs! If an entity is signable, it will be converted to a
35//! [Signable] instance.
36//!
37//! To sign a [Signable], you need to obtain a [SignableSigner].
38//! You can do this by calling [Signer::resolve_signer]. This function performs
39//! signability checks internally and returns `None` if unsignable entities are
40//! seen. So most consumers with an intent to sign should call this without
41//! calling [Signer::resolve_signability] to avoid a potentially expensive
42//! double signability test.
43//!
44//! [SignableSigner] are associated with a [Signer] and [Signable] and are
45//! used for signing just a single entity. [SignableSigner] instances are guaranteed
46//! to be capable of signing a [Signable]. However, signing operations support
47//! specifying writing the signed results to multiple types of destinations,
48//! as specified via [SigningDestination], and not every destination is supported
49//! by every input or signing backend. So before attempting signing, it is a good
50//! idea to call [SignableSigner.destination_compatibility] and verify that
51//! writing to a specific [SigningDestination] is supported!
52//!
53//! [SignableSigner] instances can further be customized to influence signing
54//! settings. See its documentation for available settings. For power users,
55//! callback functions can be registered on [Signer] instances to allow customization
56//! of the low-level signing primitives used for signing individual [Signable]. See
57//! [Signer::apple_settings_callback] and [Signer::windows_settings_callback].
58//!
59//! Finally, a signing operation can be performed via [SignableSigner::sign].
60//! This hides away all the complexity of mapping different signable entities
61//! to different signing *backends* and gives you a relatively clean interface
62//! to attempt code signing. If signing was successful, you'll get a
63//! [SignedOutput] describing where the signed content lives.
64
65use {
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
85/// URL of Apple's time-stamp protocol server.
86pub const APPLE_TIMESTAMP_URL: &str = "http://timestamp.apple.com/ts01";
87
88/// Represents a signing error.
89#[derive(Debug, Error)]
90pub enum SigningError {
91    #[error("could not determine if path is signable: {0}")]
92    SignableTestError(String),
93
94    /// General I/O error.
95    #[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/// Represents a location where signed data should be written.
157#[derive(Clone, Debug, Eq, PartialEq)]
158pub enum SigningDestination {
159    /// Sign to a file at the given path.
160    File(PathBuf),
161
162    /// Sign to a directory at the given path.
163    Directory(PathBuf),
164
165    /// Sign to data in memory.
166    Memory,
167}
168
169/// Describes capability of signing some entity to a [SigningDestination].
170#[derive(Clone, Copy, Debug, Eq, PartialEq)]
171pub enum SigningDestinationCompatibility {
172    /// Signing to a particular destination is supported.
173    Compatible,
174
175    /// Signing is not supported for a given reason.
176    Incompatible(&'static str),
177}
178
179/// Describes the output of a successful signing operation.
180#[derive(Clone, Debug, Eq, PartialEq)]
181pub enum SignedOutput {
182    /// Signed data was written to a file at a given path.
183    File(PathBuf),
184
185    /// Signed data was written to a directory at a given path.
186    Directory(PathBuf),
187
188    /// Signed data was written to memory.
189    Memory(Vec<u8>),
190}
191
192/// Represents how an entity can be signed.
193///
194/// This is used to describe what potential [SigningDestination] can be used
195/// for a signing operation and what steps the signing operation should
196/// perform (e.g. using temporary files to sign).
197#[derive(Clone, Copy, Debug, Eq, PartialEq)]
198pub enum SigningMethod {
199    /// Entity is backed by a file and can be signed in place.
200    InPlaceFile,
201
202    /// Entity is backed by a directory and can be signed in place.
203    InPlaceDirectory,
204
205    /// Entity can be signed into an arbitrary file.
206    NewFile,
207
208    /// Entity can be signed to an arbitrary directory.
209    NewDirectory,
210
211    /// Signed data can be written to memory.
212    Memory,
213}
214
215/// Represents different methods of signing that are supported.
216///
217/// Just a collection of [SigningMethod] instances.
218pub 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
228/// Represents an entity that is a candidate for signing.
229///
230/// Most users will want to use [Self::Path] or [Self::Data], which will go
231/// through signability checks and only turn into signable entities if we have
232/// a high degree of confidence that they can be signed.
233///
234/// The [Self::Forced] variant can be used to forcefully skip signability
235/// validation and supply your own [Signability]. Use this when our signability
236/// heuristics fail (please consider reporting these scenarios as bugs!). This
237/// variant is also useful for testing.
238pub enum SignableCandidate<'a> {
239    /// A filesystem path.
240    ///
241    /// Will be checked for signability.
242    Path(Cow<'a, Path>),
243
244    /// A slice of data in memory.
245    ///
246    /// Will be checked for signability.
247    Data(Cow<'a, [u8]>),
248
249    /// Entity whose [Signable] is already computed.
250    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/// Represents a known, typed entity which is signable.
326#[derive(Clone, Debug)]
327pub enum Signable {
328    /// A file that is signable on Windows.
329    WindowsFile(PathBuf),
330
331    /// Data that is signable on Windows.
332    ///
333    /// TODO store a Cow.
334    WindowsData(Vec<u8>),
335
336    /// A signable Mach-O file.
337    ///
338    /// We have to obtain the Mach-O data as part of evaluating whether it is
339    /// signable. So we keep a reference to it to avoid a re-read later.
340    MachOFile(PathBuf, Vec<u8>),
341
342    /// Signable Mach-O data.
343    MachOData(Vec<u8>),
344
345    /// An Apple bundle, persisted on the filesystem as a directory.
346    AppleBundle(PathBuf),
347}
348
349impl Signable {
350    /// Obtain signing methods that are supported.
351    pub fn signing_methods(&self) -> SigningMethods {
352        SigningMethods(match self {
353            Self::WindowsFile(_) => {
354                vec![
355                    // signtool.exe signs in place by default.
356                    SigningMethod::InPlaceFile,
357                    // We support copying to a new file and signing that.
358                    SigningMethod::NewFile,
359                    // We supporting copying to a temporary file and signing that.
360                    SigningMethod::Memory,
361                ]
362            }
363            Self::WindowsData(_) => {
364                vec![
365                    // We support writing data to a new file and signing that.
366                    SigningMethod::NewFile,
367                    // We supporting writing data to a temporary file, signing, and loading.
368                    SigningMethod::Memory,
369                ]
370            }
371            Self::MachOFile(_, _) => {
372                // apple-codesign does all of these easily.
373                vec![
374                    SigningMethod::InPlaceFile,
375                    SigningMethod::NewFile,
376                    SigningMethod::Memory,
377                ]
378            }
379            Self::MachOData(_) => {
380                // apple-codesign does all of these easily.
381                vec![SigningMethod::NewFile, SigningMethod::Memory]
382            }
383            Self::AppleBundle(_) => {
384                // apple-codesign can sign in place or to a new directory.
385                vec![SigningMethod::InPlaceDirectory, SigningMethod::NewDirectory]
386            }
387        })
388    }
389
390    /// Whether we are capable of signing.
391    pub fn is_signable(&self) -> bool {
392        !self.signing_methods().is_empty()
393    }
394
395    /// Obtain the filesystem path of the signable entity, if it is backed by a file.
396    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    /// Obtain the filesystem path of the signable directory, if it is backed by a directory.
405    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    /// Resolves the compatibility for signing this entity to a given [SigningDestination].
416    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/// Represents the results of a signability test.
489#[derive(Debug)]
490pub enum Signability {
491    /// A known entity which is signable.
492    Signable(Signable),
493
494    /// The entity is not signable for undetermined reason.
495    Unsignable,
496
497    /// The entity is a Mach-O binary that cannot be signed.
498    UnsignableMachoError(AppleCodesignError),
499
500    /// The entity is signable, but not from this platform. Details of the
501    /// limitation are stored in a string.
502    PlatformUnsupported(&'static str),
503}
504
505/// Resolve signability information given an input path.
506///
507/// The path can be to a file or directory.
508///
509/// Returns `Err` if we could not fully test the path. This includes
510/// I/O failures.
511pub 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                // But we can only sign Windows binaries on Windows since we call out to
518                // signtool.exe.
519                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            // Try to construct a signer to see if the binary is compatible.
537            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
551/// Resolve signability information given a data slice.
552pub fn data_signable(data: &[u8]) -> Result<Signability, SigningError> {
553    if tugger_windows_codesign::is_signable_binary_header(data) {
554        // But we can only sign Windows binaries on Windows since we call out to
555        // signtool.exe.
556        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        // Try to construct a signer to see if the binary is compatible.
569        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/// Represents a signing key and public certificate to sign something.
579#[derive(Debug)]
580pub enum SigningCertificate {
581    /// A parsed certificate and signing key stored in memory.
582    ///
583    /// The private key is managed by the `ring` crate.
584    Memory(CapturedX509Certificate, InMemoryPrivateKey),
585
586    /// A PFX file containing validated certificate data.
587    ///
588    /// The password to open the file is also tracked.
589    PfxFile(PathBuf, String, CapturedX509Certificate, InMemoryPrivateKey),
590
591    /// Use an automatically chosen certificate in the Windows certificate store.
592    WindowsStoreAuto,
593
594    /// A certificate stored in a Windows certificate store with a subject name string.
595    ///
596    /// See [SystemStore] for the possible system stores. [SystemStore::My] (the
597    /// current user's store) is typically where code signing certificates are
598    /// located.
599    ///
600    /// The string defines a value to match against in the certificate's `subject`
601    /// field to locate the certificate.
602    WindowsStoreSubject(SystemStore, String),
603
604    /// A certificate stored in a Windows certificate with a specified SHA-1 thumbprint.
605    ///
606    /// See [SystemStore] for the possible system stores. [SystemStore::My] (the
607    /// current user's store) is typically where code signing certificates re located.
608    ///
609    /// The string defines the SHA-1 thumbprint of the certificate. You can find this
610    /// in the `Details` tab of the certificate when viewed in `certmgr.msc`.
611    WindowsStoreSha1Thumbprint(SystemStore, String),
612}
613
614impl SigningCertificate {
615    /// Obtain an instance referencing a file containing PFX / PKCS #12 data.
616    ///
617    /// This is like [Self::from_pfx_data] except the certificate is referenced by path
618    /// instead of persisted into memory. However, we do read the certificate data
619    /// as part of constructing the instance to verify the certificate is well-formed.
620    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        // Validate the certificate is valid.
624        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    /// Obtain an instance by parsing PFX / PKCS #12 data.
636    ///
637    /// PFX data is commonly encountered in `.pfx` or `.p12` files, such as
638    /// those created when exporting certificates from Apple's `Keychain Access`
639    /// or Windows' `certmgr`.
640    ///
641    /// The contents of the file require a password to decrypt. However, if no
642    /// password was provided to create the data, this password may be the
643    /// empty string.
644    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    /// Construct an instance referring to a named certificate in a Windows certificate store.
652    ///
653    /// `store` is the name of a Windows certificate store to open. See
654    /// [SystemStore] for possible values. The `My` store (the store for the current
655    /// user) is likely where code signing certificates live.
656    ///
657    /// `subject` is a string to match against the certificate's `subject` field
658    /// to locate the certificate.
659    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    /// Construct an instance referring to a certificate with a SHA-1 thumbprint in a Windows certificate store.
670    ///
671    /// `store` is the name of a Windows certificate store to open. See
672    /// [SystemStore] for possible values. The `My` store (the store for the current
673    /// user) is likely where code signing certificates live.
674    ///
675    /// `thumbprint` is the SHA-1 thumbprint of the certificate. It should uniquely identify
676    /// any X.509 certificate.
677    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    /// Attempt to convert this instance to a [CodeSigningCertificate] for use signing on Windows.
691    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                // This requires support for materializing the certificate to a
710                // temporary file or something.
711                unimplemented!();
712            }
713        }
714    }
715}
716
717/// A callback for influencing the creation of [apple_codesign::SigningSettings]
718/// instances for a given [Signable].
719pub type AppleSigningSettingsFn =
720    fn(&Signable, &mut apple_codesign::SigningSettings) -> Result<(), anyhow::Error>;
721
722/// A callback for influencing the creation of [tugger_windows_codesign::SigntoolSign]
723/// instances for a given [Signable].
724pub type WindowsSignerFn =
725    fn(&Signable, &mut tugger_windows_codesign::SigntoolSign) -> Result<(), anyhow::Error>;
726
727/// An entity for performing code signing.
728///
729/// This contains the [SigningCertificate] as well as other global signing
730/// settings.
731pub struct Signer {
732    /// The signing certificate to use.
733    signing_certificate: SigningCertificate,
734
735    /// The certificates that signed the signing certificate.
736    ///
737    /// Ideally this contains the full certificate chain, leading to the
738    /// root CA.
739    certificate_chain: Vec<CapturedX509Certificate>,
740
741    /// URL of Time-Stamp Protocol server to use.
742    time_stamp_url: Option<Url>,
743
744    /// Optional function to influence creation of [apple_codesign::SigningSettings]
745    /// used for signing Apple signables.
746    apple_signing_settings_fn: Option<Arc<AppleSigningSettingsFn>>,
747
748    /// Optional function to influence creation of [tugger_windows_codesign::SigntoolSign]
749    /// used for signing Windows signables.
750    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    /// Construct a new instance given a [SigningCertificate].
761    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    /// Add an X.509 certificate to the certificate chain.
772    ///
773    /// When signing, it is common to include the chain of certificates
774    /// that signed the signing certificate in the signature. This can
775    /// facilitate with validation of the signature.
776    ///
777    /// This function can be called to register addition certificates
778    /// into the signing chain.
779    pub fn chain_certificate(&mut self, certificate: CapturedX509Certificate) {
780        self.certificate_chain.push(certificate);
781    }
782
783    /// Add PEM encoded X.509 certificates to the certificate chain.
784    ///
785    /// This is like [Self::chain_certificate] except the certificate is specified as
786    /// PEM encoded data. This is a human readable string like
787    /// `-----BEGIN CERTIFICATE-----` and is a common method for encoding
788    /// certificate data. The specified data can contain multiple certificates.
789    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    /// Add multiple X.509 certificates to the certificate chain.
801    ///
802    /// See [Self::chain_certificate] for details.
803    pub fn chain_certificates(
804        &mut self,
805        certificates: impl Iterator<Item = CapturedX509Certificate>,
806    ) {
807        self.certificate_chain.extend(certificates);
808    }
809
810    /// Chain X.509 certificates by searching for them in the macOS keychain.
811    ///
812    /// This function will access the macOS keychain and attempt to locate
813    /// the certificates composing the signing chain of the currently configured
814    /// signing certificate.
815    ///
816    /// This function only works when run on macOS.
817    ///
818    /// This function will error if the signing certificate wasn't self-signed
819    /// and its issuer chain could not be resolved.
820    #[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    /// Chain X.509 certificates by searching for them in the macOS keychain.
871    ///
872    /// This function will access the macOS keychain and attempt to locate
873    /// the certificates composing the signing chain of the currently configured
874    /// signing certificate.
875    ///
876    /// This function only works when run on macOS.
877    ///
878    /// This function will error if the signing certificate wasn't self-signed
879    /// and its issuer chain could not be resolved.
880    #[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    /// Set the URL of a Time-Stamp Protocol server to use.
887    ///
888    /// If specified, the server will always be used. In some cases, a
889    /// Time-Stamp Protocol server will be used automatically if one is
890    /// not specified.
891    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    /// Set a callback function to be called to influence settings for signing individual Apple signables.
898    pub fn apple_settings_callback(&mut self, cb: AppleSigningSettingsFn) {
899        self.apple_signing_settings_fn = Some(Arc::new(cb));
900    }
901
902    /// Set a callback function to be called to influence settings for signing individual Windows signables.
903    pub fn windows_settings_callback(&mut self, cb: WindowsSignerFn) {
904        self.windows_signer_fn = Some(Arc::new(cb));
905    }
906
907    /// Determine the *signability* of a potentially signable entity.
908    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        // We don't yet support exporting the key back to PFX for Windows signing.
919        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    /// Attempt to resolve a [SignableSigner] for the [SignableCandidate].
934    ///
935    /// This will determine if a given entity can be signed by us. If so, we will
936    /// return a `Some(T)` that can be used to sign it. If the entity is not signable,
937    /// returns a `None`.
938    ///
939    /// If an error occurs computing signability, `Err` occurs.
940    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
954/// A single invocation of a signing operation.
955///
956/// Instances are constructed from a [Signer] and [Signability] result and
957/// are used to sign a single item. Instances can be customized to tailor
958/// signing just the entity in question.
959pub struct SignableSigner<'a> {
960    /// The signing certificate to use.
961    signing_certificate: &'a SigningCertificate,
962
963    /// The thing we are signing.
964    signable: Signable,
965
966    /// The certificates that signed the signing certificate.
967    ///
968    /// Ideally this contains the full certificate chain, leading to the
969    /// root CA.
970    certificate_chain: Vec<CapturedX509Certificate>,
971
972    /// URL of Time-Stamp Protocol server to use.
973    time_stamp_url: Option<Url>,
974
975    /// Optional function to influence creation of [apple_codesign::SigningSettings]
976    /// used for signing Apple signables.
977    apple_signing_settings_fn: Option<Arc<AppleSigningSettingsFn>>,
978
979    /// Optional function to influence creation of [tugger_windows_codesign::SigntoolSign]
980    /// used for signing Windows signables.
981    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    /// Obtain a reference to the underlying [Signable].
1001    pub fn signable(&self) -> &Signable {
1002        &self.signable
1003    }
1004
1005    /// Obtain a [SigningDestination] that is the same as the input.
1006    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    /// Obtain a [apple_codesign::SigningSettings] from this instance.
1016    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        // Automatically register Apple CA certificates for convenience.
1036        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    /// Obtain a [tugger_windows_codesign::SigntoolSign] from this instance.
1060    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    /// Compute [SigningDestinationCompatibility] with a given [SigningDestination].
1084    ///
1085    /// This takes the current to-be-signed entity into account.
1086    pub fn destination_compatibility(
1087        &self,
1088        destination: &SigningDestination,
1089    ) -> SigningDestinationCompatibility {
1090        self.signable.destination_compatibility(destination)
1091    }
1092
1093    /// Signs this signable entity to the given destination.
1094    ///
1095    /// Callers should probably verify destination compatibility by calling
1096    /// [Self.destination_compatibility] first. But we will turn it into an
1097    /// `Err` if the destination isn't compatibile.
1098    ///
1099    /// `temp_dir` denotes the path of a writable directory where temporary
1100    /// files can be created, as needed. If not provided, a new temporary
1101    /// directory will be managed. In all cases, we attempt to remove temporary
1102    /// files as part of execution.
1103    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                // Regardless of what we're writing to, we materialize the file
1133                // data so signtool can sign it. We always go through a temp
1134                // file so we don't write to the destination except in cases
1135                // of success.
1136                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                // We may or may not be going through a temporary file. If we are,
1175                // copy the file. Otherwise sign in place.
1176                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                // TODO go through temporary directory when not doing in-place signing.
1286                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    /// Whether signing to the specified [SigningDestination] will require temporary files.
1310    ///
1311    /// Temporary files are used when:
1312    ///
1313    /// * Signed content lives in memory and signer only supports signing files.
1314    ///   (e.g. signtool.exe)
1315    /// * We are sending output to the filesystem and the destination path isn't the
1316    ///   source path. We could write directly to the destination. However, we choose
1317    ///   to play it safe and only write to the destination after signing success.
1318    ///   By going through a temporary directory, we prevent polluting the destination
1319    ///   with corrupted results.
1320    pub fn requires_temporary_files(&self, destination: &SigningDestination) -> bool {
1321        match &self.signable {
1322            // signtool only supports signing files. We'll have to persist data to a file.
1323            Signable::WindowsData(_) => true,
1324            Signable::WindowsFile(source_file) => match destination {
1325                // We don't want to touch the original file, so we'll have to create a new one.
1326                SigningDestination::Memory => true,
1327                // When signing to a file, we allow in-place signing but always go
1328                // through a temporary file when writing a new file.
1329                SigningDestination::File(dest_file) => source_file != dest_file,
1330                // Signing to a directory isn't supported.
1331                SigningDestination::Directory(_) => false,
1332            },
1333            // apple-codesign does everything in memory and doesn't need files.
1334            Signable::MachOData(_) | Signable::MachOFile(_, _) => false,
1335            // But, when we are sending output to the filesystem and the output isn't
1336            // the input, we go through a temporary directory to prevent writing
1337            // bad results to the output directory.
1338            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
1346/// Whether a request to copy between 2 paths needs to be fulfilled.
1347///
1348/// We can run into cases where we are writing to the input file but we don't
1349/// think we are because of path normalization issues. This function adds
1350/// a test for that.
1351fn 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}