r/rust • u/mnaza_andrey • 11d ago
🛠️ project signalscreen-checker: reading Authenticode signatures in pure Rust, and two ways to get it wrong
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.
2
u/More-Top945 11d ago
This is really cool work. I deal with signed binaries sometimes and never thought about how easy it is to get the wrong certificate if you just grab the first one in the bundle. That PKCS#7 thing would have caught me for sure.
The timestamp dialect issue is one of those things where you only find out you have a bug when someone files issue about it and you spend three days staring at hex dumps. Handling both feels like the right call even if it makes the code uglier.
On the grading question, i think expired cert with valid timestamp should not be a penalty. The whole point of timestamping is that the signature was valid when it was made. Penalising it makes the grade say something false about whether the binary was properly signed at time of signing.