signalscreen-checker 0.2.1

Windows code-signing hygiene checker. Reads the Authenticode signature in a PE file and grades it A-F. Pure Rust, no Windows dependency.
Documentation
//! Run the hygiene checks over decoupled `Facts` (easy to unit-test without parsing).
use serde::Serialize;

#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum Status {
    Pass,
    Warn,
    Fail,
}

#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct CheckResult {
    pub id: String,
    pub status: Status,
    pub detail: String,
}

/// Inputs the checks reason over — decoupled from the parsers.
#[derive(Debug, Clone)]
pub struct Facts {
    pub signed: bool,
    pub signature_valid: bool,
    pub digest_algo_oid: String,
    pub has_timestamp: bool,
    pub self_signed: bool,
    pub not_after_unix: i64,
    pub now_unix: i64,
}

const OID_SHA1: &str = "1.3.14.3.2.26";

pub fn run(f: &Facts) -> Vec<CheckResult> {
    let r = |id: &str, s: Status, d: &str| CheckResult {
        id: id.into(),
        status: s,
        detail: d.into(),
    };
    let mut out = Vec::new();

    if !f.signed {
        out.push(r(
            "signature",
            Status::Fail,
            "No Authenticode signature present.",
        ));
        return out; // nothing else is meaningful on an unsigned file
    }
    out.push(if f.signature_valid {
        r(
            "signature",
            Status::Pass,
            "Authenticode signature is present and well-formed.",
        )
    } else {
        r(
            "signature",
            Status::Fail,
            "Signature present but does not verify against the file.",
        )
    });
    out.push(if f.has_timestamp {
        r("timestamp", Status::Pass, "RFC-3161 timestamp present.")
    } else {
        r(
            "timestamp",
            Status::Fail,
            "No RFC-3161 timestamp; the signature stops validating once the certificate expires.",
        )
    });
    out.push(if f.digest_algo_oid == OID_SHA1 {
        r(
            "digest",
            Status::Fail,
            "SHA-1 digest is deprecated and distrusted; re-sign with SHA-256.",
        )
    } else {
        r("digest", Status::Pass, "Modern digest algorithm.")
    });
    if f.self_signed {
        out.push(r(
            "chain",
            Status::Fail,
            "Certificate is self-signed (no issuing CA); it has no reputation.",
        ));
    }
    // A timestamped signature stays valid past the signing certificate's own
    // expiry — that is the whole point of timestamping, and Authenticode still
    // accepts it with no warning. So certificate expiry (past or upcoming) does
    // not lower the grade when the file is timestamped; the report still states
    // the fact under this check so nobody is surprised later. Grade the
    // signature that was made, not the calendar.
    let days_left = (f.not_after_unix - f.now_unix) / 86_400;
    if days_left < 0 {
        out.push(if f.has_timestamp {
            r(
                "expiry",
                Status::Pass,
                "Signing certificate has expired, but the signature is timestamped, so it stays valid.",
            )
        } else {
            r("expiry", Status::Fail, "Signing certificate has expired.")
        });
    } else if days_left < 30 {
        out.push(if f.has_timestamp {
            r(
                "expiry",
                Status::Pass,
                "Signing certificate expires within 30 days, but the signature is timestamped, so its validity is unaffected.",
            )
        } else {
            r(
                "expiry",
                Status::Warn,
                "Signing certificate expires within 30 days.",
            )
        });
    } else {
        out.push(r(
            "expiry",
            Status::Pass,
            "Certificate validity is healthy.",
        ));
    }
    out
}

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

    fn base() -> Facts {
        Facts {
            signed: true,
            signature_valid: true,
            digest_algo_oid: "2.16.840.1.101.3.4.2.1".into(),
            has_timestamp: true,
            self_signed: false,
            not_after_unix: 10_000_000_000,
            now_unix: 0,
        }
    }

    #[test]
    fn unsigned_short_circuits() {
        let mut f = base();
        f.signed = false;
        let res = run(&f);
        assert_eq!(res.len(), 1);
        assert_eq!(res[0].id, "signature");
        assert_eq!(res[0].status, Status::Fail);
    }

    #[test]
    fn missing_timestamp_fails_that_check() {
        let mut f = base();
        f.has_timestamp = false;
        let ts = run(&f).into_iter().find(|c| c.id == "timestamp").unwrap();
        assert_eq!(ts.status, Status::Fail);
    }

    #[test]
    fn sha1_fails_digest() {
        let mut f = base();
        f.digest_algo_oid = "1.3.14.3.2.26".into();
        let d = run(&f).into_iter().find(|c| c.id == "digest").unwrap();
        assert_eq!(d.status, Status::Fail);
    }

    #[test]
    fn expired_cert_with_timestamp_is_not_penalized() {
        // A timestamped signature stays valid past the signing cert's expiry —
        // that is the whole point of timestamping. So an expired cert must not
        // fail the expiry check when the file is timestamped; the report still
        // states the fact under the check.
        let mut f = base(); // has_timestamp = true
        f.not_after_unix = 1_000;
        f.now_unix = 1_000 + 100 * 86_400; // expired ~100 days ago
        let e = run(&f).into_iter().find(|c| c.id == "expiry").unwrap();
        assert_eq!(e.status, Status::Pass, "detail: {}", e.detail);
        assert!(
            e.detail.contains("expired"),
            "must still state the fact: {}",
            e.detail
        );
        assert!(
            e.detail.to_lowercase().contains("timestamp"),
            "must explain why it does not count: {}",
            e.detail
        );
    }

    #[test]
    fn expired_cert_without_timestamp_still_fails() {
        let mut f = base();
        f.has_timestamp = false;
        f.not_after_unix = 1_000;
        f.now_unix = 1_000 + 100 * 86_400; // expired ~100 days ago
        let e = run(&f).into_iter().find(|c| c.id == "expiry").unwrap();
        assert_eq!(e.status, Status::Fail);
    }

    #[test]
    fn timestamped_but_expired_still_grades_a() {
        // The Reddit thread's argument, end to end: an otherwise-clean file whose
        // signing cert has expired but which carries a valid timestamp keeps its
        // full score, because the signature is still valid.
        let mut f = base();
        f.not_after_unix = 1_000;
        f.now_unix = 1_000 + 100 * 86_400; // expired ~100 days ago
        let results = run(&f);
        assert_eq!(
            crate::score::score(&results),
            100,
            "expiry must cost nothing when the signature is timestamped"
        );
    }

    #[test]
    fn self_signed_adds_chain_fail() {
        let mut f = base();
        f.self_signed = true;
        assert!(run(&f)
            .iter()
            .any(|c| c.id == "chain" && c.status == Status::Fail));
    }
}