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,
}
#[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,
pub image_hash: crate::signature::ImageHash,
}
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; }
out.push(match f.image_hash {
crate::signature::ImageHash::Mismatch => r(
"signature",
Status::Fail,
"Signature is present but does not match the file — the bytes were altered after it was signed.",
),
_ if !f.signature_valid => r(
"signature",
Status::Fail,
"Signature present but the signer certificate could not be resolved from the bundle.",
),
crate::signature::ImageHash::Match => r(
"signature",
Status::Pass,
"Authenticode signature is present and the file matches its signed hash.",
),
crate::signature::ImageHash::Unverified => r(
"signature",
Status::Pass,
"Authenticode signature is present and well-formed; image hash not verified (unsupported digest).",
),
});
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.",
));
}
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,
image_hash: crate::signature::ImageHash::Match,
}
}
fn sig_check(f: &Facts) -> CheckResult {
run(f).into_iter().find(|c| c.id == "signature").unwrap()
}
#[test]
fn image_hash_mismatch_fails_the_signature() {
let mut f = base();
f.image_hash = crate::signature::ImageHash::Mismatch;
f.signature_valid = false; let c = sig_check(&f);
assert_eq!(c.status, Status::Fail);
assert!(c.detail.contains("altered"), "detail: {}", c.detail);
}
#[test]
fn image_hash_match_passes_and_says_so() {
let c = sig_check(&base()); assert_eq!(c.status, Status::Pass);
assert!(
c.detail.contains("matches its signed hash"),
"detail: {}",
c.detail
);
}
#[test]
fn image_hash_unverified_passes_with_an_honest_note() {
let mut f = base();
f.image_hash = crate::signature::ImageHash::Unverified;
let c = sig_check(&f);
assert_eq!(c.status, Status::Pass);
assert!(
c.detail.to_lowercase().contains("not verified"),
"detail: {}",
c.detail
);
}
#[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() {
let mut f = base(); f.not_after_unix = 1_000;
f.now_unix = 1_000 + 100 * 86_400; 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; 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() {
let mut f = base();
f.not_after_unix = 1_000;
f.now_unix = 1_000 + 100 * 86_400; 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));
}
}