fix(security): Add version mismatch check, MSIX cleanup on hash failure, TraceSource diagnostics, csproj fix

- UpdateService: manifest version must match release tag_name (prevents cross-version attacks)
- UpdateService: corrupted MSIX deleted immediately on hash mismatch
- UpdateService: TraceSource logging for all verification steps
- UpdateService: Trust anchor constants with docs/release-policy.md reference
- Bootstrap: docs/release-policy.md reference next to identity constants
- Test csproj: add UpdateService.cs Compile Include (tests won't compile without this)
This commit is contained in:
ochenstarik-ui 2026-08-10 20:12:18 +07:00
parent ad396fd6a2
commit d21c0c5d22
3 changed files with 22 additions and 0 deletions

View file

@ -23,6 +23,7 @@ readonly WG_DIR="${ETC_DIR}/wireguard"
readonly FIREWALL_UNIT="ochenstarik-smm-firewall.service" readonly FIREWALL_UNIT="ochenstarik-smm-firewall.service"
readonly MESH_NETWORK="10.77.0.0/24" readonly MESH_NETWORK="10.77.0.0/24"
readonly HUB_MESH_ADDRESS="10.77.0.1/24" readonly HUB_MESH_ADDRESS="10.77.0.1/24"
# Trust anchors — see docs/release-policy.md for the full signing and identity contract.
readonly COSIGN_ISSUER="https://token.actions.githubusercontent.com" readonly COSIGN_ISSUER="https://token.actions.githubusercontent.com"
readonly COSIGN_IDENTITY_REGEXP="^https://github.com/ochenstarik-ui/server-monitor-manager/\.github/workflows/linux-release\.yml@refs/tags/v.*$" readonly COSIGN_IDENTITY_REGEXP="^https://github.com/ochenstarik-ui/server-monitor-manager/\.github/workflows/linux-release\.yml@refs/tags/v.*$"

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
@ -163,6 +164,11 @@ public class DefaultFileStorage : IFileStorage
public class UpdateService public class UpdateService
{ {
private const string Repository = "ochenstarik-ui/server-monitor-manager"; private const string Repository = "ochenstarik-ui/server-monitor-manager";
// Trust anchors pinned per docs/release-policy.md — do NOT fetch these from the release being verified.
private const string CosignIssuer = "https://token.actions.githubusercontent.com";
private const string CosignIdentityRegexp = @"^https://github\.com/ochenstarik-ui/server-monitor-manager/\.github/workflows/.*\.yml@refs/tags/v.*$";
private static readonly TraceSource Log = new("ServerMonitorManager.UpdateService", SourceLevels.All);
private readonly IHttpTransport _http; private readonly IHttpTransport _http;
private readonly ISignatureVerifier _signatureVerifier; private readonly ISignatureVerifier _signatureVerifier;
@ -239,6 +245,13 @@ public class UpdateService
} }
var version = manifestNode["version"]?.GetValue<string>(); var version = manifestNode["version"]?.GetValue<string>();
// Manifest version must match the release tag to prevent downgrade/cross-version attacks
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(releaseTagName) && version != releaseTagName)
{
Log.TraceEvent(TraceEventType.Error, 0, $"Manifest version '{version}' does not match release tag '{releaseTagName}'. Rejecting.");
throw new InvalidOperationException($"Manifest version '{version}' does not match release tag '{releaseTagName}'. Update rejected.");
}
// 3. Verify manifest signature BEFORE showing update action // 3. Verify manifest signature BEFORE showing update action
var tempFolder = _fileStorage.GetTempFolder(); var tempFolder = _fileStorage.GetTempFolder();
@ -247,7 +260,9 @@ public class UpdateService
await _fileStorage.WriteAllTextAsync(manifestPath, manifestJson, cancellationToken); await _fileStorage.WriteAllTextAsync(manifestPath, manifestJson, cancellationToken);
await _fileStorage.WriteAllTextAsync(sigPath, manifestSig, cancellationToken); await _fileStorage.WriteAllTextAsync(sigPath, manifestSig, cancellationToken);
Log.TraceEvent(TraceEventType.Information, 0, "Verifying manifest signature...");
await _signatureVerifier.VerifySignatureAsync(sigPath, manifestPath, cancellationToken); await _signatureVerifier.VerifySignatureAsync(sigPath, manifestPath, cancellationToken);
Log.TraceEvent(TraceEventType.Information, 0, "Manifest signature verified successfully.");
var msixUrl = assets.FirstOrDefault(a => a?["name"]?.GetValue<string>() == "ServerMonitorManager-win-x64.msix")?["browser_download_url"]?.GetValue<string>(); var msixUrl = assets.FirstOrDefault(a => a?["name"]?.GetValue<string>() == "ServerMonitorManager-win-x64.msix")?["browser_download_url"]?.GetValue<string>();
if (msixUrl is null) if (msixUrl is null)
@ -270,12 +285,17 @@ public class UpdateService
var tempFolder = _fileStorage.GetTempFolder(); var tempFolder = _fileStorage.GetTempFolder();
var msixPath = Path.Combine(tempFolder, "ServerMonitorManager-win-x64.msix"); var msixPath = Path.Combine(tempFolder, "ServerMonitorManager-win-x64.msix");
Log.TraceEvent(TraceEventType.Information, 0, $"Downloading update from {updateInfo.DownloadUrl}...");
await _http.DownloadFileAsync(updateInfo.DownloadUrl, msixPath, progressCallback, cancellationToken); await _http.DownloadFileAsync(updateInfo.DownloadUrl, msixPath, progressCallback, cancellationToken);
if (!VerifyFileHash(msixPath, updateInfo.ExpectedHash)) if (!VerifyFileHash(msixPath, updateInfo.ExpectedHash))
{ {
// Delete the corrupted file immediately
try { File.Delete(msixPath); } catch { /* best-effort cleanup */ }
Log.TraceEvent(TraceEventType.Error, 0, "MSIX hash mismatch — downloaded file deleted.");
throw new InvalidOperationException("Update MSIX hash mismatch. Update rejected."); throw new InvalidOperationException("Update MSIX hash mismatch. Update rejected.");
} }
Log.TraceEvent(TraceEventType.Information, 0, "MSIX hash verified successfully.");
} }
public void InstallUpdate() public void InstallUpdate()

View file

@ -6,6 +6,7 @@
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\..\src\ServerMonitorManager.Desktop\UpdateService.cs" Link="UpdateService.cs" />
<Compile Include="..\..\src\ServerMonitorManager.Desktop\SshPrivateKeySession.cs" Link="SshPrivateKeySession.cs" /> <Compile Include="..\..\src\ServerMonitorManager.Desktop\SshPrivateKeySession.cs" Link="SshPrivateKeySession.cs" />
<Compile Include="..\..\src\ServerMonitorManager.Desktop\SshHostKeyTrust.cs" Link="SshHostKeyTrust.cs" /> <Compile Include="..\..\src\ServerMonitorManager.Desktop\SshHostKeyTrust.cs" Link="SshHostKeyTrust.cs" />
<Compile Include="..\..\src\ServerMonitorManager.Desktop\SshConnectionArguments.cs" Link="SshConnectionArguments.cs" /> <Compile Include="..\..\src\ServerMonitorManager.Desktop\SshConnectionArguments.cs" Link="SshConnectionArguments.cs" />