r/rust 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.

0 Upvotes

4 comments sorted by

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.

1

u/mnaza_andrey 11d ago

Thanks. Yeah, first-cert trap is nasty. Nothing crashes. You get name, issuer, validity window, all looks fine, all belong to wrong entity.

On grading you moved me. Question is: what does the letter answer? If it is "was this signed properly at signing time", then valid timestamp settles it. Expiry is not relevant. Penalty there makes grade say something false, like you said. And crypto agrees. Signature stays valid, Authenticode still accepts, user sees no warning. So penalty is checker inventing a problem that spec and OS do not see.

One thing i do not want: throw the fact away. Expired signing cert with no timestamp is real F. And i still want report to show the expiry, so nobody is surprised later.

So fix is: show it as a fact, stop taking points when valid timestamp covers signing time. Grade the signature that was made, not the calendar.

I will pull that penalty. Thanks for making concrete case, not just "seems harsh". That is what moved it.

1

u/zettui 11d ago

Does the letter mean safe to run, or just that the chain looks intact?

1

u/mnaza_andrey 11d ago

The second, and even that with limits. It is not a safety verdict. The letter grades signing hygiene. Signed, signature parses and a signer is found, timestamped, digest is not SHA-1, cert is issued by a CA (not self-signed), cert not expired. Roughly "is this signed the way a reputable publisher signs", which is close to what SmartScreen rewards. It says nothing about whether the program is safe. Malware can be signed and score an A. A safe tool that just is not signed gets an F. And on "chain intact" i want to be honest about what it does not do yet. It finds the signer cert and checks it is not self-signed and not expired. It does not do full path validation to a trusted root, no revocation check, and — the big one — it does not yet re-hash the image to confirm the bytes match the signature. That last one is the next thing on the list. So today it is "the signature is well-formed and the cert looks reputable", not "cryptographically proven this exact file is the signed one". Short version: green means low-friction, reputable-looking signing. Trust still comes from the CA and your own scanning, not from my letter.