osslsigncode 0.1.1

In-process Rust bindings for the vendored osslsigncode Authenticode implementation
//! Path newtypes that encode Authenticode signedness.
//!
//! ```
//! use osslsigncode::{Credential, Digest, Secret, TrustAnchors, Unsigned};
//! use std::path::Path;
//!
//! let root = Path::new(env!("CARGO_MANIFEST_DIR"));
//! let dir = tempfile::tempdir().unwrap();
//! let input = root.join("vendor/osslsigncode/tests/files/unsigned.js");
//! let credential = Credential::pkcs12(root.join("tests/fixtures/publisher.p12"), Secret::value("secret"));
//!
//! let signed = Unsigned::open(&input)?
//!     .sign(credential)
//!     .digest(Digest::Sha256)
//!     .output(dir.path().join("from-unsigned.js"))
//!     .sign()?;
//! assert!(signed.path().is_file());
//! signed
//!     .verify()
//!     .trust(TrustAnchors {
//!         ca_file: Some(root.join("tests/fixtures/ca.pem")),
//!         ..Default::default()
//!     })
//!     .ignore_timestamp()
//!     .check()?;
//! # Ok::<(), osslsigncode::Error>(())
//! ```

use std::fmt;
use std::path::{Path, PathBuf};

use crate::credential::Credential;
use crate::error::{Error, Result};
use crate::jobs::{
    Add, AttachSignature, ExtractData, ExtractSignature, RemoveSignature, Sign, Verify,
    add_from_signed, attach_from, extract_data_from, extract_signature_from, remove_from,
    sign_from_unsigned,
};
use crate::native::require_readable;

macro_rules! path_handle {
    ($(#[$meta:meta])* $name:ident) => {
        $(#[$meta])*
        #[derive(Clone, Debug, Eq, PartialEq, Hash)]
        pub struct $name {
            path: PathBuf,
        }

        impl $name {
            /// Open a path as this handle type.
            pub fn open(path: impl AsRef<Path>) -> Result<Self> {
                let path = path.as_ref().to_path_buf();
                require_readable(&path, "input")?;
                Ok(Self { path })
            }

            pub(crate) fn from_path_unchecked(path: PathBuf) -> Self {
                Self { path }
            }

            /// Filesystem path.
            #[must_use]
            pub fn path(&self) -> &Path {
                &self.path
            }

            /// Consume the handle and return the path.
            #[must_use]
            pub fn into_path(self) -> PathBuf {
                self.path
            }
        }

        impl AsRef<Path> for $name {
            fn as_ref(&self) -> &Path {
                self.path()
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(formatter, "{}", self.path.display())
            }
        }

        impl TryFrom<&Path> for $name {
            type Error = Error;

            fn try_from(path: &Path) -> Result<Self> {
                Self::open(path)
            }
        }

        impl TryFrom<PathBuf> for $name {
            type Error = Error;

            fn try_from(path: PathBuf) -> Result<Self> {
                Self::open(path)
            }
        }

        impl From<$name> for PathBuf {
            fn from(value: $name) -> Self {
                value.path
            }
        }
    };
}

path_handle! {
    /// A file that is not treated as Authenticode-signed yet.
    ///
    /// Opening does not parse Authenticode; it only checks the path is readable.
    /// Use this when you intend to [`sign`](Self::sign) or
    /// [`attach_signature`](Self::attach_signature).
    Unsigned
}

impl Unsigned {
    /// Begin a sign job. Completes at [`Sign::sign`] after [`Sign::output`].
    pub fn sign(self, credential: Credential) -> Sign<crate::jobs::NeedsOutput> {
        sign_from_unsigned(self.path, credential)
    }

    /// Begin attaching an external signature. Completes at
    /// [`AttachSignature::attach`] after [`AttachSignature::output`].
    pub fn attach_signature(
        self,
        signature: impl AsRef<Path>,
    ) -> AttachSignature<crate::jobs::NeedsOutput> {
        attach_from(self.path, signature.as_ref().to_path_buf())
    }
}

path_handle! {
    /// A file produced by signing, attaching, timestamping, or opened as signed.
    Signed
}

impl Signed {
    /// Verify the embedded Authenticode signature.
    pub fn verify(&self) -> Verify {
        Verify::new(&self.path)
    }

    /// Add a timestamp or unauthenticated blob. Completes at [`Add::add`]
    /// after [`Add::output`].
    pub fn add_timestamp(&self) -> Add<crate::jobs::NeedsOutput> {
        add_from_signed(self.path.clone())
    }

    /// Extract the PKCS#7 signature. [`ExtractSignature::reader`] returns a
    /// [`Read`](std::io::Read); collect it or `std::fs::write` it as needed.
    pub fn extract_signature(&self) -> ExtractSignature {
        extract_signature_from(self.path.clone())
    }

    /// Report the signer identity, digest, and timestamp presence.
    ///
    /// This inspects the embedded PKCS#7 without asserting trust — use
    /// [`Signed::verify`] to check validity.
    pub fn inspect(&self) -> Result<crate::SignatureInfo> {
        use std::io::Read;
        let mut der = Vec::new();
        self.extract_signature()
            .reader()?
            .read_to_end(&mut der)
            .map_err(|source| Error::Io {
                field: "signature",
                path: self.path.clone(),
                source,
            })?;
        crate::inspect::from_der(&der)
    }

    /// Extract signed content. [`ExtractData::reader`] returns a
    /// [`Read`](std::io::Read); collect it or `std::fs::write` it as needed.
    pub fn extract_data(&self) -> ExtractData {
        extract_data_from(self.path.clone())
    }

    /// Remove the Authenticode signature. Completes at [`RemoveSignature::strip`]
    /// after [`RemoveSignature::output`].
    pub fn strip(&self) -> RemoveSignature<crate::jobs::NeedsOutput> {
        remove_from(self.path.clone())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::credential::{Credential, Secret};
    use crate::digest::Digest;

    #[test]
    fn typed_sign_builder_reaches_ready_without_argv() {
        let _ready = Unsigned::from_path_unchecked("app.exe".into())
            .sign(Credential::pkcs12("publisher.p12", Secret::value("x")))
            .digest(Digest::Sha256)
            .output("app-signed.exe");
    }
}