Skip to main content

signalscreen_checker/
lib.rs

1//! SignalScreen signing-hygiene checker engine.
2//!
3//! Pipeline: pe -> signature -> cert -> checks -> score -> report, wired by `analyze()`.
4
5pub mod cert;
6pub mod checks;
7pub mod pe;
8pub mod report;
9pub mod score;
10pub mod signature;
11pub mod timestamp;
12
13use anyhow::Result;
14use checks::Facts;
15use report::{CertSummary, FileInfo, Report, SigSummary, TimestampSummary};
16use sha2::{Digest, Sha256};
17
18/// Analyze PE bytes and produce a hygiene report. `now_unix` is injected for
19/// deterministic expiry checks (tests pass a fixed value; the CLI passes real time).
20pub fn analyze(name: &str, data: &[u8], now_unix: i64) -> Result<Report> {
21    let sha256 = hex::encode(Sha256::digest(data));
22
23    let (facts, signature, certificate) = match pe::extract(data)? {
24        None => (
25            Facts {
26                signed: false,
27                signature_valid: false,
28                digest_algo_oid: String::new(),
29                has_timestamp: false,
30                self_signed: false,
31                not_after_unix: 0,
32                now_unix,
33                image_hash: signature::ImageHash::Unverified,
34            },
35            SigSummary {
36                present: false,
37                valid: false,
38                image_hash: "none",
39                digest_algo: "none".into(),
40                timestamp_present: false,
41                timestamp: None,
42            },
43            None,
44        ),
45        Some(si) => {
46            let ci = si
47                .leaf_cert
48                .as_ref()
49                .map(cert::from_certificate)
50                .transpose()?;
51            let self_signed = ci.as_ref().is_some_and(|c| c.self_signed);
52            let not_after_unix = ci.as_ref().map_or(0, |c| c.not_after_unix);
53            // "valid" now means a signer was resolved AND the file's bytes match
54            // the signed digest. A mismatch (bytes altered after signing) makes the
55            // signature invalid; an unverifiable digest does not.
56            let valid =
57                si.leaf_cert.is_some() && si.image_hash != signature::ImageHash::Mismatch;
58            (
59                Facts {
60                    signed: true,
61                    signature_valid: valid,
62                    digest_algo_oid: si.digest_algo_oid.clone(),
63                    has_timestamp: si.has_timestamp,
64                    self_signed,
65                    not_after_unix,
66                    now_unix,
67                    image_hash: si.image_hash,
68                },
69                SigSummary {
70                    present: true,
71                    valid,
72                    image_hash: image_hash_str(si.image_hash),
73                    digest_algo: oid_name(&si.digest_algo_oid),
74                    timestamp_present: si.has_timestamp,
75                    timestamp: si.timestamp.as_ref().map(|t| TimestampSummary {
76                        signed_at_unix: t.signed_at_unix,
77                        authority: t.authority.clone(),
78                        kind: match t.kind {
79                            crate::timestamp::TimestampKind::Rfc3161 => "rfc3161",
80                            crate::timestamp::TimestampKind::Countersignature => {
81                                "countersignature"
82                            }
83                        },
84                    }),
85                },
86                ci.map(|c| CertSummary {
87                    subject_o: c.subject_o,
88                    issuer: c.issuer,
89                    not_after_unix: c.not_after_unix,
90                    ev_hint: c.ev_hint,
91                    self_signed: c.self_signed,
92                }),
93            )
94        }
95    };
96
97    let results = checks::run(&facts);
98    let sc = score::score(&results);
99    Ok(Report {
100        grade: score::grade(sc),
101        score: sc,
102        file: FileInfo {
103            name: name.into(),
104            sha256,
105        },
106        signature,
107        certificate,
108        checks: results,
109    })
110}
111
112/// The `image_hash` field value for the report's signature summary.
113fn image_hash_str(h: signature::ImageHash) -> &'static str {
114    match h {
115        signature::ImageHash::Match => "verified",
116        signature::ImageHash::Mismatch => "mismatch",
117        signature::ImageHash::Unverified => "unverified",
118    }
119}
120
121fn oid_name(oid: &str) -> String {
122    match oid {
123        "2.16.840.1.101.3.4.2.1" => "SHA-256".into(),
124        "1.3.14.3.2.26" => "SHA-1".into(),
125        other => other.into(),
126    }
127}