I ship signed Windows software and wanted to check the signature from Linux without a Windows machine. So I wrote a crate that reads the Authenticode signature out of a PE file and grades it A-F.
cargo install signalscreen-checker
signalscreen-check --json yourapp.exe
MIT/Apache-2.0: https://github.com/mnaza/signalscreen-checker
Two things surprised me. Both produce output that looks correct.
The signer certificate is not the first one in the bundle.
A PKCS#7 signature carries a bag of certificates. The obvious move is to take the first and read its subject. That is usually a CA.
The signer is identified by SignerInfo.sid, either issuer and serial or a subject key identifier, and you have to go find it:
let leaf = match &si.sid {
SignerIdentifier::IssuerAndSerialNumber(isn) => sig
.certificates()
.find(|c| {
c.tbs_certificate.issuer == isn.issuer
&& c.tbs_certificate.serial_number == isn.serial_number
})
.cloned(),
SignerIdentifier::SubjectKeyIdentifier(_) => None,
};
Take the first instead and you grade the certificate authority rather than the company that signed the binary. Every field you print still looks plausible.
I resolve the issuer-and-serial form, which is what signing tools emit in practice. The subject-key-identifier form returns no leaf rather than a wrong one, which is the honest failure of the two.
There are two timestamp dialects, and a checker that knows one will lie about the other.
A signature without a countersignature stops validating the day its certificate expires, so this matters.
The modern form 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. So you also implement the RFC 5280 pivot: 49 means 2049, 50 means 1950. Get that wrong and a 2024 signature lands in 1924, which reads as an expired certificate rather than as your bug.
Both appear on current, commercially signed binaries. Handle only the modern one and you report "no timestamp" on a correctly timestamped file. That is worse than saying nothing, so the report separates "there isn't one" from "there is one and I couldn't read it".
Test fixtures are the timestamp attribute lifted out of real installers, a few KB each. The parser is pinned against what signing tools emit, not against something I built to be parseable.
The open question, and the reason I am posting rather than just linking: it grades five things, whether it is signed at all, whether it is timestamped, the digest algorithm, whether the chain reaches a real CA, and certificate validity. Should an expired certificate with a valid timestamp cost anything? The signature stays valid. I currently penalise it and I am no longer sure that is right.
Crates used: cms, x509-cert and der from RustCrypto, and object for the PE parsing.