Problem
UpdateService.VerifyAuthenticode (Services/UpdateService.cs:410-438) extracts the signer certificate and returns true after only logging cert.Subject (lines 421-422). There is no subject/thumbprint pinning and no chain build. So once SysManager is signed, a binary signed by ANY certificate - including an attacker's own self-issued one - passes this gate identically to a legitimately signed build. The same codebase already contains the correct pattern for a third-party binary: SpeedTestService.VerifyOoklaSignature (Services/SpeedTestService.cs:443-484) pins the subject (line 452: !cert.Subject.Contains("Ookla") -> delete + throw), then explicitly builds an X509Chain with RevocationMode.Online (lines 463-471) because, in its own comment at line 466-467, "Subject alone is forgeable (anyone can issue a self-signed 'Ookla' cert)". The asymmetry is stark: the downloaded Ookla CLI gets pinned-subject + full chain + revocation + fail-closed deletion, while SysManager's own self-replacing, potentially-elevated update binary gets a log line. Grepped for any pinning constant (expectedSigner|signerSubject|CN=laurentiu|pinnedSubject|ExpectedSubject) -> no matches. Grepped UpdateServiceAuthenticodeTests.cs for Subject|signer|publisher -> no matches; its four tests only cover unsigned/tiny/empty/MZ-header files.
Proposed solution
Restructure VerifyAuthenticode into a two-state contract now, while it is cheap: (a) no embedded signature -> allowed, log it, as today (correct while builds are unsigned); (b) signature present -> pin it the way VerifyOoklaSignature does - build an X509Chain with RevocationMode.Online and compare the signer against a pinned expected subject/thumbprint constant, failing closed on mismatch. Introduce the pinned value as a single named constant that is empty/inactive until a certificate exists, so switching signing on is a one-line change rather than a security redesign under time pressure. Add the missing negative tests to SysManager.Tests/UpdateServiceAuthenticodeTests.cs: a signed-but-wrong-publisher file must be rejected. Reference SpeedTestService.cs:443-484 in the code comment so the two paths stay visibly symmetric.
Rationale
This is the one place where the trust posture is pre-wired to fail the moment the maintainer's biggest planned trust upgrade lands. Today the exposure is bounded, because SHA256 (verified over a held deny-write handle) is the real gate and the no-signature branch is the live one. But the day a certificate arrives, the natural assumption will be "we sign now, so the signature check protects us" - and it will not, silently. Fixing it before signing ships costs one function and two tests; fixing it after means auditing a security control while also managing a certificate rollout. The codebase already proves the maintainer knows the correct pattern, so this is closing an internal inconsistency, not inventing a subsystem.
Evidence
Read Services/UpdateService.cs:391-438 in full: line 396 private const int CryptENoMatch = unchecked((int)0x80092009);, line 421 Serilog.Log.Information("Update binary is Authenticode-signed: {Subject}", cert.Subject); immediately followed by return true; at 422 - no subject comparison, no X509Chain anywhere in the method. Read Services/SpeedTestService.cs:438-484 for contrast: subject pin at 452, chain build at 463-471, TryDeleteExe + throw on both failure paths. Grepped for pinning identifiers across SysManager/ excluding obj/ -> exit 1 (none). Read SysManager.Tests/UpdateServiceAuthenticodeTests.cs in full: four tests, all path-shape cases (UnsignedFile->true, TinyUnsignedFile->true, EmptyFile->false, RandomBinaryContent->true); grep for Subject/signer/publisher -> exit 1.
Risk / trade-off
Adding a chain build with online revocation introduces a network dependency and latency into the install path, and could fail-closed on an offline machine - so the chain check must apply only in the signature-present branch (never blocking today's unsigned flow) and needs a considered offline policy. Over-tightening could brick self-update for users who legitimately obtained a build signed under a future, rotated certificate, so the pinned value should tolerate certificate renewal (pin on subject/organization rather than a single thumbprint, or maintain a small allowed set).
Affected area
About
Effort: M | priority: value 3/5, fit 4/5
Identified during the trust, distribution and reach audit audit.
Problem
UpdateService.VerifyAuthenticode (Services/UpdateService.cs:410-438) extracts the signer certificate and returns true after only logging cert.Subject (lines 421-422). There is no subject/thumbprint pinning and no chain build. So once SysManager is signed, a binary signed by ANY certificate - including an attacker's own self-issued one - passes this gate identically to a legitimately signed build. The same codebase already contains the correct pattern for a third-party binary: SpeedTestService.VerifyOoklaSignature (Services/SpeedTestService.cs:443-484) pins the subject (line 452:
!cert.Subject.Contains("Ookla")-> delete + throw), then explicitly builds an X509Chain withRevocationMode.Online(lines 463-471) because, in its own comment at line 466-467, "Subject alone is forgeable (anyone can issue a self-signed 'Ookla' cert)". The asymmetry is stark: the downloaded Ookla CLI gets pinned-subject + full chain + revocation + fail-closed deletion, while SysManager's own self-replacing, potentially-elevated update binary gets a log line. Grepped for any pinning constant (expectedSigner|signerSubject|CN=laurentiu|pinnedSubject|ExpectedSubject) -> no matches. Grepped UpdateServiceAuthenticodeTests.cs forSubject|signer|publisher-> no matches; its four tests only cover unsigned/tiny/empty/MZ-header files.Proposed solution
Restructure VerifyAuthenticode into a two-state contract now, while it is cheap: (a) no embedded signature -> allowed, log it, as today (correct while builds are unsigned); (b) signature present -> pin it the way VerifyOoklaSignature does - build an X509Chain with RevocationMode.Online and compare the signer against a pinned expected subject/thumbprint constant, failing closed on mismatch. Introduce the pinned value as a single named constant that is empty/inactive until a certificate exists, so switching signing on is a one-line change rather than a security redesign under time pressure. Add the missing negative tests to SysManager.Tests/UpdateServiceAuthenticodeTests.cs: a signed-but-wrong-publisher file must be rejected. Reference SpeedTestService.cs:443-484 in the code comment so the two paths stay visibly symmetric.
Rationale
This is the one place where the trust posture is pre-wired to fail the moment the maintainer's biggest planned trust upgrade lands. Today the exposure is bounded, because SHA256 (verified over a held deny-write handle) is the real gate and the no-signature branch is the live one. But the day a certificate arrives, the natural assumption will be "we sign now, so the signature check protects us" - and it will not, silently. Fixing it before signing ships costs one function and two tests; fixing it after means auditing a security control while also managing a certificate rollout. The codebase already proves the maintainer knows the correct pattern, so this is closing an internal inconsistency, not inventing a subsystem.
Evidence
Read Services/UpdateService.cs:391-438 in full: line 396
private const int CryptENoMatch = unchecked((int)0x80092009);, line 421Serilog.Log.Information("Update binary is Authenticode-signed: {Subject}", cert.Subject);immediately followed byreturn true;at 422 - no subject comparison, no X509Chain anywhere in the method. Read Services/SpeedTestService.cs:438-484 for contrast: subject pin at 452, chain build at 463-471,TryDeleteExe+ throw on both failure paths. Grepped for pinning identifiers across SysManager/ excluding obj/ -> exit 1 (none). Read SysManager.Tests/UpdateServiceAuthenticodeTests.cs in full: four tests, all path-shape cases (UnsignedFile->true, TinyUnsignedFile->true, EmptyFile->false, RandomBinaryContent->true); grep for Subject/signer/publisher -> exit 1.Risk / trade-off
Adding a chain build with online revocation introduces a network dependency and latency into the install path, and could fail-closed on an offline machine - so the chain check must apply only in the signature-present branch (never blocking today's unsigned flow) and needs a considered offline policy. Over-tightening could brick self-update for users who legitimately obtained a build signed under a future, rotated certificate, so the pinned value should tolerate certificate renewal (pin on subject/organization rather than a single thumbprint, or maintain a small allowed set).
Affected area
About
Effort: M | priority: value 3/5, fit 4/5
Identified during the trust, distribution and reach audit audit.