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
//! SignalScreen signing-hygiene checker engine.
//!
//! Pipeline: pe -> signature -> cert -> checks -> score -> report, wired by `analyze()`.

pub mod cert;
pub mod checks;
pub mod pe;
pub mod report;
pub mod score;
pub mod signature;
pub mod timestamp;

use anyhow::Result;
use checks::Facts;
use report::{CertSummary, FileInfo, Report, SigSummary, TimestampSummary};
use sha2::{Digest, Sha256};

/// Analyze PE bytes and produce a hygiene report. `now_unix` is injected for
/// deterministic expiry checks (tests pass a fixed value; the CLI passes real time).
pub fn analyze(name: &str, data: &[u8], now_unix: i64) -> Result<Report> {
    let sha256 = hex::encode(Sha256::digest(data));

    let (facts, signature, certificate) = match pe::extract(data)? {
        None => (
            Facts {
                signed: false,
                signature_valid: false,
                digest_algo_oid: String::new(),
                has_timestamp: false,
                self_signed: false,
                not_after_unix: 0,
                now_unix,
                image_hash: signature::ImageHash::Unverified,
            },
            SigSummary {
                present: false,
                valid: false,
                image_hash: "none",
                digest_algo: "none".into(),
                timestamp_present: false,
                timestamp: None,
            },
            None,
        ),
        Some(si) => {
            let ci = si
                .leaf_cert
                .as_ref()
                .map(cert::from_certificate)
                .transpose()?;
            let self_signed = ci.as_ref().is_some_and(|c| c.self_signed);
            let not_after_unix = ci.as_ref().map_or(0, |c| c.not_after_unix);
            // "valid" now means a signer was resolved AND the file's bytes match
            // the signed digest. A mismatch (bytes altered after signing) makes the
            // signature invalid; an unverifiable digest does not.
            let valid =
                si.leaf_cert.is_some() && si.image_hash != signature::ImageHash::Mismatch;
            (
                Facts {
                    signed: true,
                    signature_valid: valid,
                    digest_algo_oid: si.digest_algo_oid.clone(),
                    has_timestamp: si.has_timestamp,
                    self_signed,
                    not_after_unix,
                    now_unix,
                    image_hash: si.image_hash,
                },
                SigSummary {
                    present: true,
                    valid,
                    image_hash: image_hash_str(si.image_hash),
                    digest_algo: oid_name(&si.digest_algo_oid),
                    timestamp_present: si.has_timestamp,
                    timestamp: si.timestamp.as_ref().map(|t| TimestampSummary {
                        signed_at_unix: t.signed_at_unix,
                        authority: t.authority.clone(),
                        kind: match t.kind {
                            crate::timestamp::TimestampKind::Rfc3161 => "rfc3161",
                            crate::timestamp::TimestampKind::Countersignature => {
                                "countersignature"
                            }
                        },
                    }),
                },
                ci.map(|c| CertSummary {
                    subject_o: c.subject_o,
                    issuer: c.issuer,
                    not_after_unix: c.not_after_unix,
                    ev_hint: c.ev_hint,
                    self_signed: c.self_signed,
                }),
            )
        }
    };

    let results = checks::run(&facts);
    let sc = score::score(&results);
    Ok(Report {
        grade: score::grade(sc),
        score: sc,
        file: FileInfo {
            name: name.into(),
            sha256,
        },
        signature,
        certificate,
        checks: results,
    })
}

/// The `image_hash` field value for the report's signature summary.
fn image_hash_str(h: signature::ImageHash) -> &'static str {
    match h {
        signature::ImageHash::Match => "verified",
        signature::ImageHash::Mismatch => "mismatch",
        signature::ImageHash::Unverified => "unverified",
    }
}

fn oid_name(oid: &str) -> String {
    match oid {
        "2.16.840.1.101.3.4.2.1" => "SHA-256".into(),
        "1.3.14.3.2.26" => "SHA-1".into(),
        other => other.into(),
    }
}