signalscreen-checker 0.3.0

Windows code-signing hygiene checker. Reads the Authenticode signature in a PE file and grades it A-F. Pure Rust, no Windows dependency.
Documentation
//! Extract hygiene-relevant facts from the signer's X.509 certificate.
use anyhow::Result;
use x509_cert::Certificate;

#[derive(Debug, Clone, PartialEq)]
pub struct CertInfo {
    pub subject_o: Option<String>,
    pub issuer: String,
    pub not_after_unix: i64,
    pub self_signed: bool,
    pub ev_hint: bool,
}

// certificatePolicies extension OID.
const OID_CERT_POLICIES: &str = "2.5.29.32";
// DER body bytes (after tag+len) of EV code-signing policy OID 2.23.140.1.3.
const EV_OID_BODY: &[u8] = &[0x67, 0x81, 0x0c, 0x01, 0x03];

pub fn from_certificate(cert: &Certificate) -> Result<CertInfo> {
    let tbs = &cert.tbs_certificate;
    let subject = tbs.subject.to_string();
    let issuer = tbs.issuer.to_string();
    let subject_o = extract_rdn(&subject, "O");
    let self_signed = tbs.subject == tbs.issuer;
    let not_after_unix = tbs.validity.not_after.to_unix_duration().as_secs() as i64;
    let ev_hint = tbs.extensions.as_ref().is_some_and(|exts| {
        exts.iter().any(|e| {
            e.extn_id.to_string() == OID_CERT_POLICIES
                && contains_subslice(e.extn_value.as_bytes(), EV_OID_BODY)
        })
    });
    Ok(CertInfo {
        subject_o,
        issuer,
        not_after_unix,
        self_signed,
        ev_hint,
    })
}

/// Pull a single RDN value (e.g. "O") out of an RFC 4514 name.
///
/// Values may carry escaped commas. `O=DigiCert\, Inc.` is one
/// field, so splitting on every comma returns `DigiCert\` and
/// drops the rest. That is a real issuer of a real timestamping
/// certificate, so the naive version was wrong where it showed.
pub(crate) fn extract_rdn(dn: &str, key: &str) -> Option<String> {
    let prefix = format!("{key}=");
    let mut fields = Vec::new();
    let mut current = String::new();
    let mut chars = dn.chars();
    while let Some(c) = chars.next() {
        match c {
            '\\' => {
                if let Some(next) = chars.next() {
                    current.push(next);
                }
            }
            ',' => fields.push(std::mem::take(&mut current)),
            _ => current.push(c),
        }
    }
    fields.push(current);
    fields
        .into_iter()
        .find_map(|f| f.trim().strip_prefix(&prefix).map(|v| v.to_string()))
        .filter(|v| !v.is_empty())
}

fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
    needle.len() <= haystack.len() && haystack.windows(needle.len()).any(|w| w == needle)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn escaped_commas_in_a_dn_do_not_split_the_field() {
        // A real issuer. Splitting on every comma returned
        // "DigiCert\\" and lost the rest.
        let dn = r"C=US,O=DigiCert\, Inc.,CN=DigiCert Trusted G4 CA";
        assert_eq!(extract_rdn(dn, "O").as_deref(), Some("DigiCert, Inc."));
        assert_eq!(extract_rdn(dn, "C").as_deref(), Some("US"));
    }

    #[test]
    fn extract_rdn_works() {
        assert_eq!(
            extract_rdn("O=Acme Corp,CN=Foo", "O"),
            Some("Acme Corp".to_string())
        );
        assert_eq!(extract_rdn("CN=Foo,C=US", "O"), None);
    }

    #[test]
    fn parses_selfsigned_leaf() {
        let data = std::fs::read("fixtures/selfsigned-sha256.exe").unwrap();
        let sig = crate::pe::extract(&data).unwrap().unwrap();
        let leaf = sig.leaf_cert.unwrap();
        let info = from_certificate(&leaf).unwrap();
        assert!(info.self_signed);
        assert_eq!(info.subject_o.as_deref(), Some("SignalScreen Test"));
        assert!(!info.ev_hint);
    }
}