Repository navigation
fix(update): check the installer's Authenticode signer before running it - #469
Merged
Merged
Conversation
The installer and its SHA256SUMS file sit on the same GitHub release, so the checksum only shows the download is intact: whoever could replace the installer could replace the checksum too. The updater now also asks WinVerifyTrust whether the installer's signature is trusted (revocation included) and compares its signer with the publisher that signed the running Filee. A different or missing signer discards the installer and falls back to the download page. Copies that are not signed themselves (built from source) keep to the checksum. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KnifeLemon
requested changes
Oct 23, 2026
KnifeLemon
left a comment
Owner
There was a problem hiding this comment.
Thanks a lot @Bddgc, this is a good addition, and the timing is right: the next release is the first one we sign (Certum, "Open Source Developer MYEONGGU KIM"), so the check would be active from the first signed copy on.
I went through it and tried it:
Authenticode.TrustedSigneron a Setup signed with our certificate returnsCN=Open Source Developer MYEONGGU KIM, O=Open Source Developer, L=Seoul, S=Seoul, C=KR, with and without the revocation check, and null for the unsigned 2.0.0 Setup. The struct layout, theCRYPT_PROVIDER_CERToffset and the closing call look right to me.- Updating from 2.0.0 (unsigned) to the first signed release keeps working: 2.0.0 doesn't have this check and only compares the SHA-256. From the first signed release on, the installed
Filee.exeis signed (our release script checks that after a test install), so the check applies to every update after that. - It builds on top of main, also together with #408, and
UpdateServiceTestspass.
One change before we merge:
CheckPublisherruns on the UI thread. After the download,UpdateAsynccontinues on the dispatcher, and WinVerifyTrust hashes the whole file and, with the revocation check, contacts the CA. Here it took about 0.7 s for a 23 MB signed file, so the ~125 MB Setup would freeze the window for a few seconds, longer on a slow network. Please run it off the UI thread, for exampleawait Task.Run(() => CheckPublisher(installer));. The exceptions still reach the existingcatch.
Optional, your call:
- When the revocation check can't reach the CA (proxy, firewall, offline), WinVerifyTrust fails with
CERT_E_REVOCATION_FAILURE, and the log then says the installer "is signed by no publisher Windows trusts", which points people the wrong way. Logging the WinVerifyTrust result would make that clear. Ending at the download page in that case is fine with us.
On our side, we'll keep every Windows release signed with a certificate that has exactly this subject, including when we renew it next year. Thanks!
3 of 5 tasks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cation check WinVerifyTrust hashes the whole installer and may ask the certificate authority about revocation, which froze the window for a few seconds on the ~125 MB Setup. Run CheckPublisher on the thread pool; its exceptions still reach the existing catch. When the installer's signature only fails the revocation check (offline, a proxy or a firewall, or a revoked certificate), the log now says so instead of "signed by no publisher Windows trusts". The installer is still refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this change?
Before the in-app updater runs a downloaded installer, it now checks that Windows trusts the installer's Authenticode signature and that the publisher who signed the running Filee signed it too. Today it only checks the SHA-256 from
Filee-<version>-SHA256SUMS.txt, which sits on the same release as the installer: anyone who could replace the installer (a leaked token, a compromised account or workflow) could replace the checksum file as well, and every installed copy would run the replacement elevated.When the protection takes effect
The published releases are not signed yet. As of v2.0.0, neither
Filee-2.0.0-win-Setup.exenorFilee.exein the portable zip has an Authenticode signature (Get-AuthenticodeSignature→NotSigned).A copy that isn't signed itself has no publisher to compare with, so it keeps to the SHA-256 check exactly as today. This change therefore does not break updates from any version released so far. The protection starts with the first version that ships signed (
build/sign-release.ps1): from then on, that copy refuses any installer not signed by the same publisher. After that, every Windows release has to stay signed with the same certificate subject, or signed copies will refuse to update in-app and send users to the download page instead.Details
src/Filee.Platform.Windows/Authenticode.cs(new):Authenticode.TrustedSigner(path, checkRevocation)asksWinVerifyTrust(WINTRUST_ACTION_GENERIC_VERIFY_V2, no UI) whether the signature is trusted, then reads the signer certificate from that same verification state (WTHelperProvDataFromStateData→ signer → certificate) and returns its subject. It returns null for an unsigned or untrusted file, and off Windows.UpdateService.CheckPublisherruns between the download andProcess.Start. The installer is checked with revocation (whole chain, root excluded); the running copy without. A different or missing signer deletes the installer and throwsInvalidDataException, which takes the existing failure path (alert, then the download page).SECURITY.mddescribes the new check.Type of change
How was it tested?
dotnet format --verify-no-changes(on the changed files,--severity warnas in CI)dotnet test:Filee.App.Testsfiltered toUpdateServiceTests(30 passed, 7 of them new), Windows 11, .NET SDK 10.0.402; the rest of the suite was not runNew tests: installer from the same publisher is kept; installer from another publisher, or unsigned/untrusted, is deleted and refused; the installer is checked with revocation and the running copy without; an unsigned copy keeps to the checksum;
TrustedSignerreturns null for an unsigned file and a Microsoft subject fordotnet.exe(a realWinVerifyTrustcall, skipped off Windows).dotnet build -c Releaseadds no warnings.Checklist
src/Filee.App/Assets/i18n/*.jsonfiles (none added)THIRD-PARTY-NOTICES.md(none added)🤖 Generated with Claude Code