osslsigncode 0.1.1

In-process Rust bindings for the vendored osslsigncode Authenticode implementation
//! Read the signer identity out of an embedded Authenticode signature.
//!
//! [`crate::Signed::inspect`] extracts the PKCS#7 and parses it here.
//! It answers *who signed this, with what, and is it timestamped* without
//! asserting trust; use [`crate::Verify`] for validity.
//!
//! ```
//! use osslsigncode::{Credential, Digest, Secret, 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("inspect.js"))
//!     .sign()?;
//! let info = signed.inspect()?;
//! assert_eq!(info.digest, Some(Digest::Sha256));
//! assert_eq!(info.signers.len(), 1);
//! assert!(info.signers[0].subject.contains("CN=osslsigncode-doctest"));
//! assert!(!info.timestamped);
//! # Ok::<(), osslsigncode::Error>(())
//! ```

use std::ffi::CStr;
use std::os::raw::{c_char, c_int};
use std::ptr;

use foreign_types::{ForeignType, ForeignTypeRef};
use openssl::nid::Nid;
use openssl::pkcs7::Pkcs7;
use openssl::stack::StackRef;
use openssl::x509::{X509, X509NameRef, X509Ref};
use openssl_sys::{
    ASN1_OBJECT, OBJ_obj2nid, OBJ_obj2txt, OPENSSL_sk_free, OPENSSL_sk_num, OPENSSL_sk_value,
    PKCS7_SIGNER_INFO, PKCS7_get_signer_info, PKCS7_get0_signers, X509_ALGOR_get0,
};

use crate::digest::Digest;
use crate::error::{Error, Result};

/// RFC-3161 timestamp token attribute (`spcRfc3161`).
const OID_RFC3161: &str = "1.3.6.1.4.1.311.3.3.1";
/// Legacy Authenticode timestamp (PKCS#9 counter-signature).
const OID_COUNTERSIGNATURE: &str = "1.2.840.113549.1.9.6";

/// Signer identity and metadata read from an Authenticode signature.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct SignatureInfo {
    /// One entry per signer certificate (usually one).
    pub signers: Vec<Signer>,
    /// Message-digest algorithm named in the signature, if recognized.
    pub digest: Option<Digest>,
    /// Whether the signature carries an RFC-3161 or legacy timestamp token.
    pub timestamped: bool,
}

/// A single Authenticode signer certificate.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct Signer {
    /// Subject distinguished name, e.g. `CN=Example, O=Example Inc`.
    pub subject: String,
    /// Issuer distinguished name.
    pub issuer: String,
    /// Certificate serial number, hex-encoded.
    pub serial: String,
    /// `notBefore` validity bound, as OpenSSL renders it.
    pub not_before: String,
    /// `notAfter` validity bound.
    pub not_after: String,
}

pub(crate) fn from_der(der: &[u8]) -> Result<SignatureInfo> {
    let pkcs7 = Pkcs7::from_der(der).map_err(|error| Error::Runtime {
        message: format!("failed to parse PKCS#7 signature: {error}"),
    })?;
    // SAFETY: `pkcs7` owns the object for the duration of this function; all
    // pointers below borrow from it and are only read.
    let raw = pkcs7.as_ptr();
    Ok(SignatureInfo {
        signers: collect_signers(raw),
        digest: first_signer_digest(raw),
        timestamped: is_timestamped(raw),
    })
}

fn collect_signers(pkcs7: *mut openssl_sys::PKCS7) -> Vec<Signer> {
    // PKCS7_get0_signers returns a freshly allocated stack whose X509 elements
    // are borrowed (get0), so free the stack but not the certificates.
    let stack = unsafe { PKCS7_get0_signers(pkcs7, ptr::null_mut(), 0) };
    if stack.is_null() {
        return Vec::new();
    }
    let signers = unsafe { StackRef::<X509>::from_ptr(stack) }
        .iter()
        .map(signer_from_cert)
        .collect();
    unsafe { OPENSSL_sk_free(stack.cast()) };
    signers
}

fn signer_from_cert(cert: &X509Ref) -> Signer {
    Signer {
        subject: name_string(cert.subject_name()),
        issuer: name_string(cert.issuer_name()),
        serial: cert
            .serial_number()
            .to_bn()
            .and_then(|bn| bn.to_hex_str())
            .map(|hex| hex.to_string())
            .unwrap_or_default(),
        not_before: cert.not_before().to_string(),
        not_after: cert.not_after().to_string(),
    }
}

/// Render a distinguished name as `SN=value, SN=value`.
fn name_string(name: &X509NameRef) -> String {
    name.entries()
        .map(|entry| {
            let key = entry.object().nid().short_name().unwrap_or("?");
            let value = String::from_utf8_lossy(entry.data().as_slice());
            format!("{key}={value}")
        })
        .collect::<Vec<_>>()
        .join(", ")
}

fn first_signer_info(pkcs7: *mut openssl_sys::PKCS7) -> *mut PKCS7_SIGNER_INFO {
    let infos = unsafe { PKCS7_get_signer_info(pkcs7) };
    if infos.is_null() || unsafe { OPENSSL_sk_num(infos.cast()) } < 1 {
        return ptr::null_mut();
    }
    unsafe { OPENSSL_sk_value(infos.cast(), 0) }.cast()
}

fn first_signer_digest(pkcs7: *mut openssl_sys::PKCS7) -> Option<Digest> {
    let info = first_signer_info(pkcs7);
    if info.is_null() {
        return None;
    }
    let algorithm = unsafe { (*info).digest_alg };
    if algorithm.is_null() {
        return None;
    }
    let mut object: *const ASN1_OBJECT = ptr::null();
    unsafe { X509_ALGOR_get0(&mut object, ptr::null_mut(), ptr::null_mut(), algorithm) };
    if object.is_null() {
        return None;
    }
    Digest::try_from(Nid::from_raw(unsafe { OBJ_obj2nid(object) })).ok()
}

impl TryFrom<Nid> for Digest {
    /// Not every NID is a digest osslsigncode supports.
    type Error = ();

    fn try_from(nid: Nid) -> std::result::Result<Self, Self::Error> {
        match nid {
            Nid::MD5 => Ok(Self::Md5),
            Nid::SHA1 => Ok(Self::Sha1),
            Nid::SHA256 => Ok(Self::Sha256),
            Nid::SHA384 => Ok(Self::Sha384),
            Nid::SHA512 => Ok(Self::Sha512),
            _ => Err(()),
        }
    }
}

fn is_timestamped(pkcs7: *mut openssl_sys::PKCS7) -> bool {
    let info = first_signer_info(pkcs7);
    if info.is_null() {
        return false;
    }
    let attributes = unsafe { (*info).unauth_attr };
    if attributes.is_null() {
        return false;
    }
    let count = unsafe { OPENSSL_sk_num(attributes.cast()) };
    (0..count).any(|index| {
        let attribute = unsafe { OPENSSL_sk_value(attributes.cast(), index) };
        let object = unsafe { openssl_sys::X509_ATTRIBUTE_get0_object(attribute.cast()) };
        matches!(
            oid_text(object).as_deref(),
            Some(OID_RFC3161 | OID_COUNTERSIGNATURE)
        )
    })
}

/// Numeric OID string for an `ASN1_OBJECT`, e.g. `1.2.840.113549.1.9.6`.
fn oid_text(object: *const ASN1_OBJECT) -> Option<String> {
    if object.is_null() {
        return None;
    }
    let mut buffer = [0 as c_char; 128];
    let written = unsafe { OBJ_obj2txt(buffer.as_mut_ptr(), buffer.len() as c_int, object, 1) };
    if written <= 0 {
        return None;
    }
    unsafe { CStr::from_ptr(buffer.as_ptr()) }
        .to_str()
        .ok()
        .map(str::to_owned)
}