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:
parent
ad396fd6a2
commit
d21c0c5d22
3 changed files with 22 additions and 0 deletions
|
|
@ -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.*$"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue