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

signalscreen-checker

Windows code-signing hygiene checker. Reads the Authenticode signature inside a PE file and grades it A–F.

Pure Rust, no Windows dependency. Runs on Linux, macOS and Windows.

This is the engine behind the free checker at signalscreen.dev.

crates.io docs.rs downloads licence: MIT OR Apache-2.0

Quick start

cargo install signalscreen-checker

signalscreen-check yourapp.exe          # human-readable grade
signalscreen-check yourapp.exe --json   # canonical JSON
$ signalscreen-check fixtures/selfsigned-sha256.exe
Signing hygiene: F (20/100)
  file: fixtures/selfsigned-sha256.exe
  [Pass] signature: Authenticode signature is present and well-formed.
  [Fail] timestamp: No RFC-3161 timestamp; the signature stops validating once the certificate expires.
  [Pass] digest: Modern digest algorithm.
  [Fail] chain: Certificate is self-signed (no issuing CA); it has no reputation.
  [Pass] expiry: Certificate validity is healthy.

Timestamps: there are two dialects, not one

A signature without a countersignature stops validating the day its certificate expires. So the checker asks whether one is there. It also reads it.

Two formats are in the wild. The modern one is an RFC 3161 token in a Microsoft unsigned attribute. The older one is a PKCS#9 countersignature carrying signingTime, as a UTCTime with a two-digit year.

Signing tools pick one and never say which. Both appear on current, commercially signed binaries. Handle only the modern form and you report "no timestamp" on a correctly timestamped file — a false accusation, and worse than saying nothing.

The report gives the date and the authority for either:

"timestamp": {
  "signed_at_unix": 1711455192,
  "authority": "DigiCert, Inc.",
  "kind": "countersignature"
}

timestamp_present without timestamp means one is there and could not be read. That is deliberate. Collapsing the two would report a hardening failure where there is only a parsing one.

What it does not do

Saying this first, because the distinction is the whole point.

A grade here is not a SmartScreen prediction. SmartScreen makes a reputation judgement. Reputation is separate from whether the certificate is valid. A properly signed release with grade A can still show "Windows protected your PC". This crate only says whether the signing is clean. Necessary, not sufficient.

Two more limits, so nothing is oversold:

  • signature.valid means the signature parsed and a signer certificate was found. The image hash is not recomputed and compared.
  • Both PE32 and PE32+ parse, but only the 64-bit path has a signed fixture. The signature lives in the attribute certificate table, located the same way for both widths, so the 32-bit signed path should work. It is not proven by a test.

CLI

Human-readable by default. --json emits the canonical output that every other surface (the web app, CI) consumes:

signalscreen-check installer.exe --json

Library

use signalscreen_checker::analyze;

let data = std::fs::read("installer.exe")?;
let now = std::time::SystemTime::now()
    .duration_since(std::time::UNIX_EPOCH)?
    .as_secs() as i64;

let report = analyze("installer.exe", &data, now)?;
println!("{} ({}/100)", report.grade, report.score);

now_unix is injected, not read inside. Keeps expiry checks deterministic under test.

Grading

Score starts at 100. Each failing check subtracts.

Check Condition Penalty
signature no Authenticode signature 100 (grade F outright)
timestamp no RFC-3161 timestamp 40
chain self-signed 40
digest SHA-1 digest algorithm 30
expiry certificate expired, and not timestamped 20
expiry expires within 30 days, and not timestamped 10 (warning)

A 90–100 · B 80–89 · C 70–79 · D 60–69 · F below 60.

Weights encode a thesis, not a standard. Missing timestamp costs as much as a self-signed chain. Reason: when the certificate expires, an untimestamped signature stops validating everywhere, retroactively, on binaries shipped years ago.

A timestamped signature is exempt from the expiry penalty. Timestamping is exactly what keeps a signature valid past its certificate's expiry, and Authenticode accepts it with no warning — so taking points off would make the grade assert something false about whether the binary was properly signed. The expiry is still shown as a fact in the report; it just no longer costs points when a timestamp covers it.

One subtlety

The signer's leaf certificate is selected by SignerInfo.sid. Not by taking the first entry of the PKCS#7 certificate bag.

That bag holds the whole bundle, leaf plus intermediates plus root, in no guaranteed order. First entry is very often a CA. Naive code grades it and looks like it works: a CA certificate is not self-signed the way the leaf would be, and its expiry is years away.

API_NOTES.md records the parts of the authenticode 0.4 API this depends on, verified against real signed binaries.

Tests

cargo test

15 tests. Fixtures are self-contained. unsigned.exe and unsigned-pe32.exe are compiled do-nothing PEs, 64- and 32-bit. selfsigned-sha256.exe is the 64-bit one signed with a throwaway test CA. No third-party binary is committed.

Licence

Dual-licensed under either Apache-2.0 or MIT, at your option.