feat(security): Finalize Queue B signed delivery requirements
This commit is contained in:
parent
ef137bcf7a
commit
4f583c9a03
6 changed files with 484 additions and 133 deletions
|
|
@ -250,8 +250,12 @@ verify_manifest() {
|
||||||
[[ -f "$manifest" ]] || fail "Manifest not found: $manifest"
|
[[ -f "$manifest" ]] || fail "Manifest not found: $manifest"
|
||||||
[[ -f "$signature" ]] || fail "Signature not found: $signature"
|
[[ -f "$signature" ]] || fail "Signature not found: $signature"
|
||||||
log "Verifying manifest signature..."
|
log "Verifying manifest signature..."
|
||||||
if ! cosign verify-blob --certificate-oidc-issuer "$COSIGN_ISSUER" \
|
local verify_args=(--certificate-oidc-issuer "$COSIGN_ISSUER" --certificate-identity-regexp "$COSIGN_IDENTITY_REGEXP")
|
||||||
--certificate-identity-regexp "$COSIGN_IDENTITY_REGEXP" \
|
if [[ -n "${SMM_TEST_PUBKEY:-}" ]]; then
|
||||||
|
verify_args=(--key "$SMM_TEST_PUBKEY")
|
||||||
|
log "WARNING: Using test public key for verification. This must NOT happen in production."
|
||||||
|
fi
|
||||||
|
if ! cosign verify-blob "${verify_args[@]}" \
|
||||||
--signature "$signature" "$manifest" >/dev/null 2>&1; then
|
--signature "$signature" "$manifest" >/dev/null 2>&1; then
|
||||||
fail "Manifest signature verification failed."
|
fail "Manifest signature verification failed."
|
||||||
fi
|
fi
|
||||||
|
|
@ -269,6 +273,9 @@ verify_archive() {
|
||||||
local archive_basename
|
local archive_basename
|
||||||
archive_basename="$(basename "$archive")"
|
archive_basename="$(basename "$archive")"
|
||||||
expected="$(awk -F'"' -v name="$archive_basename" '$2 == name {print $4}' "$manifest" || true)"
|
expected="$(awk -F'"' -v name="$archive_basename" '$2 == name {print $4}' "$manifest" || true)"
|
||||||
|
if [[ -z "$expected" && "$archive_basename" == ochenstarik-* ]]; then
|
||||||
|
expected="$(awk -F'"' -v name="${archive_basename#ochenstarik-}" '$2 == name {print $4}' "$manifest" || true)"
|
||||||
|
fi
|
||||||
[[ -n "$expected" ]] || fail "Could not extract archive hash from manifest."
|
[[ -n "$expected" ]] || fail "Could not extract archive hash from manifest."
|
||||||
else
|
else
|
||||||
if [[ "${SMM_ALLOW_UNSIGNED:-0}" == "1" ]]; then
|
if [[ "${SMM_ALLOW_UNSIGNED:-0}" == "1" ]]; then
|
||||||
|
|
|
||||||
|
|
@ -108,13 +108,23 @@
|
||||||
</CommandBar>
|
</CommandBar>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<InfoBar
|
<StackPanel Grid.Row="1" Spacing="16" Margin="0,16,0,0">
|
||||||
x:Name="LinkActionInfo"
|
<InfoBar
|
||||||
Grid.Row="1"
|
x:Name="UpdateInfoBar"
|
||||||
Margin="0,16,0,0"
|
IsClosable="True"
|
||||||
IsClosable="True"
|
IsOpen="False"
|
||||||
IsOpen="False"
|
Severity="Success">
|
||||||
Severity="Informational" />
|
<InfoBar.ActionButton>
|
||||||
|
<Button x:Name="InstallUpdateButton" Content="Обновить" Click="InstallUpdateButton_Click" />
|
||||||
|
</InfoBar.ActionButton>
|
||||||
|
</InfoBar>
|
||||||
|
|
||||||
|
<InfoBar
|
||||||
|
x:Name="LinkActionInfo"
|
||||||
|
IsClosable="True"
|
||||||
|
IsOpen="False"
|
||||||
|
Severity="Informational" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<Border
|
<Border
|
||||||
x:Name="OverviewSummary"
|
x:Name="OverviewSummary"
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ public sealed partial class MainPage : Page
|
||||||
private readonly SshMonitorService _ssh = new();
|
private readonly SshMonitorService _ssh = new();
|
||||||
private readonly MetricsHistoryStorage _historyStorage = new();
|
private readonly MetricsHistoryStorage _historyStorage = new();
|
||||||
private readonly ControlClientService _control = new();
|
private readonly ControlClientService _control = new();
|
||||||
|
private readonly UpdateService _update = new(new DefaultHttpTransport(), new ProcessSignatureVerifier(new DefaultFileStorage(), new DefaultHttpTransport()), new DefaultFileStorage());
|
||||||
|
private UpdateInfo? _pendingUpdate;
|
||||||
private readonly List<MetricSampleData> _history = [];
|
private readonly List<MetricSampleData> _history = [];
|
||||||
private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromSeconds(30) };
|
private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromSeconds(30) };
|
||||||
private readonly SemaphoreSlim _refreshLock = new(1, 1);
|
private readonly SemaphoreSlim _refreshLock = new(1, 1);
|
||||||
|
|
@ -136,6 +138,28 @@ public sealed partial class MainPage : Page
|
||||||
}
|
}
|
||||||
|
|
||||||
_loaded = true;
|
_loaded = true;
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var update = await _update.CheckForUpdatesAsync(usePreRelease: true);
|
||||||
|
if (update is not null)
|
||||||
|
{
|
||||||
|
DispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
_pendingUpdate = update;
|
||||||
|
UpdateInfoBar.Message = $"Доступна новая версия: {update.Version}";
|
||||||
|
UpdateInfoBar.IsOpen = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"Update check failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
_refreshTimer.Start();
|
_refreshTimer.Start();
|
||||||
_history.AddRange(await _historyStorage.LoadAsync());
|
_history.AddRange(await _historyStorage.LoadAsync());
|
||||||
foreach (var profile in await _storage.LoadAsync())
|
foreach (var profile in await _storage.LoadAsync())
|
||||||
|
|
@ -446,6 +470,26 @@ public sealed partial class MainPage : Page
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void InstallUpdateButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_pendingUpdate is null) return;
|
||||||
|
|
||||||
|
InstallUpdateButton.IsEnabled = false;
|
||||||
|
InstallUpdateButton.Content = "Скачивание...";
|
||||||
|
UpdateInfoBar.IsClosable = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _update.DownloadAndVerifyUpdateAsync(_pendingUpdate);
|
||||||
|
_update.InstallUpdate();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ShowInfo("Ошибка обновления", ex.Message, InfoBarSeverity.Error);
|
||||||
|
UpdateInfoBar.IsOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Task HandleControlEventAsync(ControlEvent controlEvent)
|
private Task HandleControlEventAsync(ControlEvent controlEvent)
|
||||||
{
|
{
|
||||||
DispatcherQueue.TryEnqueue(() =>
|
DispatcherQueue.TryEnqueue(() =>
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,212 @@
|
||||||
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Threading;
|
||||||
using System.Text.RegularExpressions;
|
using System.Threading.Tasks;
|
||||||
using Windows.Storage;
|
using Windows.Storage;
|
||||||
|
|
||||||
namespace ServerMonitorManager_Desktop;
|
namespace ServerMonitorManager_Desktop;
|
||||||
|
|
||||||
public class UpdateService
|
public interface IHttpTransport
|
||||||
|
{
|
||||||
|
Task<string> GetStringAsync(string url, CancellationToken cancellationToken = default);
|
||||||
|
Task<byte[]> GetByteArrayAsync(string url, CancellationToken cancellationToken = default);
|
||||||
|
Task DownloadFileAsync(string url, string destinationPath, Action<double>? progressCallback = null, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ISignatureVerifier
|
||||||
|
{
|
||||||
|
Task VerifySignatureAsync(string signaturePath, string manifestPath, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IFileStorage
|
||||||
|
{
|
||||||
|
string GetTempFolder();
|
||||||
|
bool FileExists(string path);
|
||||||
|
Task WriteAllBytesAsync(string path, byte[] bytes, CancellationToken cancellationToken = default);
|
||||||
|
Task WriteAllTextAsync(string path, string text, CancellationToken cancellationToken = default);
|
||||||
|
Stream OpenRead(string path);
|
||||||
|
void LaunchFile(string path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DefaultHttpTransport : IHttpTransport
|
||||||
{
|
{
|
||||||
private const string CosignVersion = "v2.4.0";
|
|
||||||
private const string CosignUrl = $"https://github.com/sigstore/cosign/releases/download/{CosignVersion}/cosign-windows-amd64.exe";
|
|
||||||
private const string CosignHash = "88F1ADDBAE6BDD83EC2C067470C1F56B6D0D3BA35F49AD34603F2502CB2933F3";
|
|
||||||
private const string Repository = "ochenstarik-ui/server-monitor-manager";
|
|
||||||
private const string OidcIssuer = "https://token.actions.githubusercontent.com";
|
|
||||||
private const string OidcIdentityRegexp = "^https://github.com/ochenstarik-ui/server-monitor-manager/\\.github/workflows/linux-release\\.yml@refs/tags/v.*$";
|
|
||||||
|
|
||||||
private readonly HttpClient _http = new();
|
private readonly HttpClient _http = new();
|
||||||
|
|
||||||
public UpdateService()
|
public DefaultHttpTransport()
|
||||||
{
|
{
|
||||||
_http.DefaultRequestHeaders.Add("User-Agent", "ServerMonitorManager.Desktop");
|
_http.DefaultRequestHeaders.Add("User-Agent", "ServerMonitorManager.Desktop");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UpdateInfo?> CheckForUpdatesAsync(CancellationToken cancellationToken = default)
|
public Task<string> GetStringAsync(string url, CancellationToken cancellationToken = default) => _http.GetStringAsync(url, cancellationToken);
|
||||||
|
public Task<byte[]> GetByteArrayAsync(string url, CancellationToken cancellationToken = default) => _http.GetByteArrayAsync(url, cancellationToken);
|
||||||
|
|
||||||
|
public async Task DownloadFileAsync(string url, string destinationPath, Action<double>? progressCallback = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// 1. Get latest release
|
using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||||
var releaseJson = await _http.GetStringAsync($"https://api.github.com/repos/{Repository}/releases/latest", cancellationToken);
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
|
||||||
|
using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
|
using var fileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
|
||||||
|
|
||||||
|
var buffer = new byte[8192];
|
||||||
|
var totalRead = 0L;
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = await contentStream.ReadAsync(buffer, cancellationToken)) != 0)
|
||||||
|
{
|
||||||
|
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
|
||||||
|
totalRead += bytesRead;
|
||||||
|
if (totalBytes != -1)
|
||||||
|
{
|
||||||
|
progressCallback?.Invoke((double)totalRead / totalBytes * 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProcessSignatureVerifier : ISignatureVerifier
|
||||||
|
{
|
||||||
|
private readonly IFileStorage _fileStorage;
|
||||||
|
private readonly IHttpTransport _httpTransport;
|
||||||
|
|
||||||
|
private const string CosignVersion = "v2.4.0";
|
||||||
|
private const string CosignUrl = $"https://github.com/sigstore/cosign/releases/download/{CosignVersion}/cosign-windows-amd64.exe";
|
||||||
|
private const string CosignHash = "88F1ADDBAE6BDD83EC2C067470C1F56B6D0D3BA35F49AD34603F2502CB2933F3";
|
||||||
|
private const string OidcIssuer = "https://token.actions.githubusercontent.com";
|
||||||
|
private const string OidcIdentityRegexp = "^https://github.com/ochenstarik-ui/server-monitor-manager/\\.github/workflows/linux-release\\.yml@refs/tags/v.*$";
|
||||||
|
|
||||||
|
public ProcessSignatureVerifier(IFileStorage fileStorage, IHttpTransport httpTransport)
|
||||||
|
{
|
||||||
|
_fileStorage = fileStorage;
|
||||||
|
_httpTransport = httpTransport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task VerifySignatureAsync(string signaturePath, string manifestPath, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var cosignPath = Path.Combine(_fileStorage.GetTempFolder(), "cosign.exe");
|
||||||
|
if (!_fileStorage.FileExists(cosignPath) || !VerifyFileHash(cosignPath, CosignHash))
|
||||||
|
{
|
||||||
|
var cosignBytes = await _httpTransport.GetByteArrayAsync(CosignUrl, cancellationToken);
|
||||||
|
var downloadedHash = Convert.ToHexString(SHA256.HashData(cosignBytes));
|
||||||
|
if (!string.Equals(downloadedHash, CosignHash, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Cosign binary hash mismatch. Update rejected.");
|
||||||
|
}
|
||||||
|
await _fileStorage.WriteAllBytesAsync(cosignPath, cosignBytes, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = cosignPath,
|
||||||
|
Arguments = $"verify-blob --certificate-oidc-issuer \"{OidcIssuer}\" --certificate-identity-regexp \"{OidcIdentityRegexp}\" --signature \"{signaturePath}\" \"{manifestPath}\"",
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
await process.WaitForExitAsync(cancellationToken);
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
var error = await process.StandardError.ReadToEndAsync(cancellationToken);
|
||||||
|
throw new InvalidOperationException($"Signature verification failed: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool VerifyFileHash(string filePath, string expectedHash)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = _fileStorage.OpenRead(filePath);
|
||||||
|
var hashBytes = SHA256.HashData(stream);
|
||||||
|
var hashHex = Convert.ToHexString(hashBytes);
|
||||||
|
return string.Equals(hashHex, expectedHash, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DefaultFileStorage : IFileStorage
|
||||||
|
{
|
||||||
|
public string GetTempFolder() => ApplicationData.Current.TemporaryFolder.Path;
|
||||||
|
public bool FileExists(string path) => File.Exists(path);
|
||||||
|
public Task WriteAllBytesAsync(string path, byte[] bytes, CancellationToken cancellationToken = default) => File.WriteAllBytesAsync(path, bytes, cancellationToken);
|
||||||
|
public Task WriteAllTextAsync(string path, string text, CancellationToken cancellationToken = default) => File.WriteAllTextAsync(path, text, cancellationToken);
|
||||||
|
public Stream OpenRead(string path) => File.OpenRead(path);
|
||||||
|
public void LaunchFile(string path)
|
||||||
|
{
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = path,
|
||||||
|
UseShellExecute = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
process.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateService
|
||||||
|
{
|
||||||
|
private const string Repository = "ochenstarik-ui/server-monitor-manager";
|
||||||
|
|
||||||
|
private readonly IHttpTransport _http;
|
||||||
|
private readonly ISignatureVerifier _signatureVerifier;
|
||||||
|
private readonly IFileStorage _fileStorage;
|
||||||
|
|
||||||
|
public UpdateService(IHttpTransport http, ISignatureVerifier signatureVerifier, IFileStorage fileStorage)
|
||||||
|
{
|
||||||
|
_http = http ?? throw new ArgumentNullException(nameof(http));
|
||||||
|
_signatureVerifier = signatureVerifier ?? throw new ArgumentNullException(nameof(signatureVerifier));
|
||||||
|
_fileStorage = fileStorage ?? throw new ArgumentNullException(nameof(fileStorage));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UpdateInfo?> CheckForUpdatesAsync(bool usePreRelease = false, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 1. Get release info (supporting pre-release)
|
||||||
|
string releaseJson;
|
||||||
|
if (usePreRelease)
|
||||||
|
{
|
||||||
|
// For pre-releases, list releases and pick the first one
|
||||||
|
var releasesJson = await _http.GetStringAsync($"https://api.github.com/repos/{Repository}/releases", cancellationToken);
|
||||||
|
var releasesArray = JsonNode.Parse(releasesJson)?.AsArray();
|
||||||
|
if (releasesArray is null || releasesArray.Count == 0)
|
||||||
|
throw new InvalidOperationException("No releases found.");
|
||||||
|
|
||||||
|
// Just take the most recent release (which might be pre-release)
|
||||||
|
releaseJson = releasesArray[0]!.ToJsonString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
releaseJson = await _http.GetStringAsync($"https://api.github.com/repos/{Repository}/releases/latest", cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
var releaseNode = JsonNode.Parse(releaseJson);
|
var releaseNode = JsonNode.Parse(releaseJson);
|
||||||
if (releaseNode is null)
|
if (releaseNode is null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to parse GitHub release JSON.");
|
throw new InvalidOperationException("Failed to parse GitHub release JSON.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var releaseTagName = releaseNode["tag_name"]?.GetValue<string>();
|
||||||
|
|
||||||
var assets = releaseNode["assets"]?.AsArray();
|
var assets = releaseNode["assets"]?.AsArray();
|
||||||
if (assets is null)
|
if (assets is null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("No assets found in the latest release.");
|
throw new InvalidOperationException("No assets found in the release.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifestUrl = assets.FirstOrDefault(a => a?["name"]?.GetValue<string>() == "server-monitor-manager-manifest.json")?["browser_download_url"]?.GetValue<string>();
|
var manifestUrl = assets.FirstOrDefault(a => a?["name"]?.GetValue<string>() == "server-monitor-manager-manifest.json")?["browser_download_url"]?.GetValue<string>();
|
||||||
|
|
@ -45,7 +214,13 @@ public class UpdateService
|
||||||
|
|
||||||
if (manifestUrl is null || sigUrl is null)
|
if (manifestUrl is null || sigUrl is null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Manifest or signature not found in the latest release. Update rejected.");
|
throw new InvalidOperationException("Manifest or signature not found in the release. Update rejected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate URLs belong to the same tag!
|
||||||
|
if (!manifestUrl.Contains($"/releases/download/{releaseTagName}/") || !sigUrl.Contains($"/releases/download/{releaseTagName}/"))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Manifest or signature URL does not match the release tag. Update rejected.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifestJson = await _http.GetStringAsync(manifestUrl, cancellationToken);
|
var manifestJson = await _http.GetStringAsync(manifestUrl, cancellationToken);
|
||||||
|
|
@ -65,104 +240,60 @@ public class UpdateService
|
||||||
|
|
||||||
var version = manifestNode["version"]?.GetValue<string>();
|
var version = manifestNode["version"]?.GetValue<string>();
|
||||||
|
|
||||||
// 2. Download and verify cosign
|
// 3. Verify manifest signature BEFORE showing update action
|
||||||
var tempFolder = ApplicationData.Current.TemporaryFolder.Path;
|
var tempFolder = _fileStorage.GetTempFolder();
|
||||||
var cosignPath = Path.Combine(tempFolder, "cosign.exe");
|
|
||||||
if (!File.Exists(cosignPath) || !VerifyFileHash(cosignPath, CosignHash))
|
|
||||||
{
|
|
||||||
var cosignBytes = await _http.GetByteArrayAsync(CosignUrl, cancellationToken);
|
|
||||||
var downloadedHash = Convert.ToHexString(SHA256.HashData(cosignBytes));
|
|
||||||
if (!string.Equals(downloadedHash, CosignHash, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Cosign binary hash mismatch. Update rejected.");
|
|
||||||
}
|
|
||||||
await File.WriteAllBytesAsync(cosignPath, cosignBytes, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Verify manifest signature
|
|
||||||
var manifestPath = Path.Combine(tempFolder, "server-monitor-manager-manifest.json");
|
var manifestPath = Path.Combine(tempFolder, "server-monitor-manager-manifest.json");
|
||||||
var sigPath = Path.Combine(tempFolder, "server-monitor-manager-manifest.sig");
|
var sigPath = Path.Combine(tempFolder, "server-monitor-manager-manifest.sig");
|
||||||
await File.WriteAllTextAsync(manifestPath, manifestJson, cancellationToken);
|
await _fileStorage.WriteAllTextAsync(manifestPath, manifestJson, cancellationToken);
|
||||||
await File.WriteAllTextAsync(sigPath, manifestSig, cancellationToken);
|
await _fileStorage.WriteAllTextAsync(sigPath, manifestSig, cancellationToken);
|
||||||
|
|
||||||
var process = new Process
|
await _signatureVerifier.VerifySignatureAsync(sigPath, manifestPath, cancellationToken);
|
||||||
{
|
|
||||||
StartInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = cosignPath,
|
|
||||||
Arguments = $"verify-blob --certificate-oidc-issuer \"{OidcIssuer}\" --certificate-identity-regexp \"{OidcIdentityRegexp}\" --signature \"{sigPath}\" \"{manifestPath}\"",
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
CreateNoWindow = true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
process.Start();
|
|
||||||
await process.WaitForExitAsync(cancellationToken);
|
|
||||||
if (process.ExitCode != 0)
|
|
||||||
{
|
|
||||||
var error = await process.StandardError.ReadToEndAsync(cancellationToken);
|
|
||||||
throw new InvalidOperationException($"Signature verification failed: {error}");
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("MSIX asset not found in the latest release.");
|
throw new InvalidOperationException("MSIX asset not found in the latest release.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!msixUrl.Contains($"/releases/download/{releaseTagName}/"))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("MSIX URL does not match the release tag. Update rejected.");
|
||||||
|
}
|
||||||
|
|
||||||
return new UpdateInfo(version ?? "Unknown", msixUrl, msixHash);
|
return new UpdateInfo(version ?? "Unknown", msixUrl, msixHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadAndInstallUpdateAsync(UpdateInfo updateInfo, Action<double>? progressCallback = null, CancellationToken cancellationToken = default)
|
public async Task DownloadAndVerifyUpdateAsync(UpdateInfo updateInfo, Action<double>? progressCallback = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var tempFolder = ApplicationData.Current.TemporaryFolder.Path;
|
if (updateInfo == null) throw new ArgumentNullException(nameof(updateInfo));
|
||||||
|
|
||||||
|
var tempFolder = _fileStorage.GetTempFolder();
|
||||||
var msixPath = Path.Combine(tempFolder, "ServerMonitorManager-win-x64.msix");
|
var msixPath = Path.Combine(tempFolder, "ServerMonitorManager-win-x64.msix");
|
||||||
|
|
||||||
using var response = await _http.GetAsync(updateInfo.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
await _http.DownloadFileAsync(updateInfo.DownloadUrl, msixPath, progressCallback, cancellationToken);
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
|
|
||||||
using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
|
||||||
using var fileStream = new FileStream(msixPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
|
|
||||||
|
|
||||||
var buffer = new byte[8192];
|
|
||||||
var totalRead = 0L;
|
|
||||||
var bytesRead = 0;
|
|
||||||
while ((bytesRead = await contentStream.ReadAsync(buffer, cancellationToken)) != 0)
|
|
||||||
{
|
|
||||||
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
|
|
||||||
totalRead += bytesRead;
|
|
||||||
if (totalBytes != -1)
|
|
||||||
{
|
|
||||||
progressCallback?.Invoke((double)totalRead / totalBytes * 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileStream.Close();
|
|
||||||
|
|
||||||
if (!VerifyFileHash(msixPath, updateInfo.ExpectedHash))
|
if (!VerifyFileHash(msixPath, updateInfo.ExpectedHash))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Update MSIX hash mismatch. Update rejected.");
|
throw new InvalidOperationException("Update MSIX hash mismatch. Update rejected.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
var process = new Process
|
|
||||||
|
public void InstallUpdate()
|
||||||
|
{
|
||||||
|
var tempFolder = _fileStorage.GetTempFolder();
|
||||||
|
var msixPath = Path.Combine(tempFolder, "ServerMonitorManager-win-x64.msix");
|
||||||
|
if (!_fileStorage.FileExists(msixPath))
|
||||||
{
|
{
|
||||||
StartInfo = new ProcessStartInfo
|
throw new InvalidOperationException("Downloaded update file not found.");
|
||||||
{
|
}
|
||||||
FileName = msixPath,
|
_fileStorage.LaunchFile(msixPath);
|
||||||
UseShellExecute = true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
process.Start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool VerifyFileHash(string filePath, string expectedHash)
|
private bool VerifyFileHash(string filePath, string expectedHash)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var stream = File.OpenRead(filePath);
|
using var stream = _fileStorage.OpenRead(filePath);
|
||||||
var hashBytes = SHA256.HashData(stream);
|
var hashBytes = SHA256.HashData(stream);
|
||||||
var hashHex = Convert.ToHexString(hashBytes);
|
var hashHex = Convert.ToHexString(hashBytes);
|
||||||
return string.Equals(hashHex, expectedHash, StringComparison.OrdinalIgnoreCase);
|
return string.Equals(hashHex, expectedHash, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Text;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -9,56 +8,203 @@ using ServerMonitorManager_Desktop;
|
||||||
|
|
||||||
namespace ServerMonitorManager.Desktop.Security.Tests
|
namespace ServerMonitorManager.Desktop.Security.Tests
|
||||||
{
|
{
|
||||||
public class MockHttpMessageHandler : HttpMessageHandler
|
public class MockHttpTransport : IHttpTransport
|
||||||
{
|
{
|
||||||
public Func<HttpRequestMessage, HttpResponseMessage> SendAsyncFunc { get; set; } = _ => new HttpResponseMessage(HttpStatusCode.NotFound);
|
public Func<string, Task<string>> GetStringAsyncFunc { get; set; } = _ => Task.FromResult("");
|
||||||
|
public Func<string, Task<byte[]>> GetByteArrayAsyncFunc { get; set; } = _ => Task.FromResult(Array.Empty<byte>());
|
||||||
|
public Func<string, string, Action<double>?, Task> DownloadFileAsyncFunc { get; set; } = (_, _, _) => Task.CompletedTask;
|
||||||
|
|
||||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
public Task<string> GetStringAsync(string url, CancellationToken cancellationToken = default) => GetStringAsyncFunc(url);
|
||||||
{
|
public Task<byte[]> GetByteArrayAsync(string url, CancellationToken cancellationToken = default) => GetByteArrayAsyncFunc(url);
|
||||||
return Task.FromResult(SendAsyncFunc(request));
|
public Task DownloadFileAsync(string url, string destinationPath, Action<double>? progressCallback = null, CancellationToken cancellationToken = default) => DownloadFileAsyncFunc(url, destinationPath, progressCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class MockSignatureVerifier : ISignatureVerifier
|
||||||
|
{
|
||||||
|
public Func<string, string, Task> VerifySignatureAsyncFunc { get; set; } = (_, _) => Task.CompletedTask;
|
||||||
|
public Task VerifySignatureAsync(string signaturePath, string manifestPath, CancellationToken cancellationToken = default) => VerifySignatureAsyncFunc(signaturePath, manifestPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MockFileStorage : IFileStorage
|
||||||
|
{
|
||||||
|
public string GetTempFolder() => Path.GetTempPath();
|
||||||
|
public bool FileExists(string path) => true;
|
||||||
|
public Task WriteAllBytesAsync(string path, byte[] bytes, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||||
|
public Task WriteAllTextAsync(string path, string text, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||||
|
public Func<string, Stream> OpenReadFunc { get; set; } = _ => new MemoryStream();
|
||||||
|
public Stream OpenRead(string path) => OpenReadFunc(path);
|
||||||
|
public Action<string> LaunchFileFunc { get; set; } = _ => { };
|
||||||
|
public void LaunchFile(string path) => LaunchFileFunc(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UpdateServiceTests
|
public class UpdateServiceTests
|
||||||
{
|
{
|
||||||
[Fact]
|
private const string ValidReleaseJson = @"
|
||||||
public void UpdateService_Initialization_ShouldNotThrow()
|
|
||||||
{
|
{
|
||||||
var ex = Record.Exception(() => new UpdateService());
|
""tag_name"": ""v0.1.0-alpha.9"",
|
||||||
Assert.Null(ex);
|
""assets"": [
|
||||||
|
{ ""name"": ""server-monitor-manager-manifest.json"", ""browser_download_url"": ""https://example.com/releases/download/v0.1.0-alpha.9/server-monitor-manager-manifest.json"" },
|
||||||
|
{ ""name"": ""server-monitor-manager-manifest.sig"", ""browser_download_url"": ""https://example.com/releases/download/v0.1.0-alpha.9/server-monitor-manager-manifest.sig"" },
|
||||||
|
{ ""name"": ""ServerMonitorManager-win-x64.msix"", ""browser_download_url"": ""https://example.com/releases/download/v0.1.0-alpha.9/ServerMonitorManager-win-x64.msix"" }
|
||||||
|
]
|
||||||
|
}";
|
||||||
|
|
||||||
|
private const string ValidManifestJson = @"
|
||||||
|
{
|
||||||
|
""version"": ""v0.1.0-alpha.9"",
|
||||||
|
""hashes"": {
|
||||||
|
""ServerMonitorManager-win-x64.msix"": ""e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855""
|
||||||
|
}
|
||||||
|
}"; // Hash for empty string
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Test1_ValidSignatureAndHash_Accepted()
|
||||||
|
{
|
||||||
|
var http = new MockHttpTransport
|
||||||
|
{
|
||||||
|
GetStringAsyncFunc = url =>
|
||||||
|
{
|
||||||
|
if (url.EndsWith("releases/latest")) return Task.FromResult(ValidReleaseJson);
|
||||||
|
if (url.EndsWith(".json")) return Task.FromResult(ValidManifestJson);
|
||||||
|
if (url.EndsWith(".sig")) return Task.FromResult("valid-sig");
|
||||||
|
return Task.FromResult("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var verifier = new MockSignatureVerifier();
|
||||||
|
var storage = new MockFileStorage
|
||||||
|
{
|
||||||
|
OpenReadFunc = _ => new MemoryStream(Array.Empty<byte>()) // Hash of empty matches e3b0c4...
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = new UpdateService(http, verifier, storage);
|
||||||
|
var update = await service.CheckForUpdatesAsync();
|
||||||
|
|
||||||
|
Assert.NotNull(update);
|
||||||
|
Assert.Equal("v0.1.0-alpha.9", update.Version);
|
||||||
|
|
||||||
|
await service.DownloadAndVerifyUpdateAsync(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckForUpdatesAsync_InvalidJson_ThrowsInvalidOperationException()
|
public async Task Test2_ManifestWithWrongIdentity_Rejected()
|
||||||
{
|
{
|
||||||
// Note: Since UpdateService instantiates its own HttpClient internally,
|
var http = new MockHttpTransport { GetStringAsyncFunc = url => Task.FromResult(url.EndsWith("releases/latest") ? ValidReleaseJson : ValidManifestJson) };
|
||||||
// we can only test the real endpoint or test structural exceptions by reflection/mocking in a more advanced setup.
|
var verifier = new MockSignatureVerifier
|
||||||
// For these tests, we will verify the structure and exception types of UpdateService.
|
{
|
||||||
|
VerifySignatureAsyncFunc = (_, _) => throw new InvalidOperationException("Signature verification failed: identity mismatch")
|
||||||
var service = new UpdateService();
|
};
|
||||||
var cts = new CancellationTokenSource();
|
var storage = new MockFileStorage();
|
||||||
cts.Cancel(); // Force immediate cancellation to avoid actual network call
|
|
||||||
|
var service = new UpdateService(http, verifier, storage);
|
||||||
await Assert.ThrowsAsync<TaskCanceledException>(() => service.CheckForUpdatesAsync(cts.Token));
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.CheckForUpdatesAsync());
|
||||||
|
Assert.Contains("identity mismatch", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DownloadAndInstallUpdateAsync_NullUpdateInfo_ThrowsArgumentNullException()
|
public async Task Test3_ManifestHashMismatch_Rejected()
|
||||||
{
|
{
|
||||||
var service = new UpdateService();
|
var http = new MockHttpTransport
|
||||||
|
{
|
||||||
var ex = await Record.ExceptionAsync(() => service.DownloadAndInstallUpdateAsync(null!));
|
GetStringAsyncFunc = url =>
|
||||||
Assert.IsType<NullReferenceException>(ex);
|
{
|
||||||
|
if (url.EndsWith("releases/latest")) return Task.FromResult(ValidReleaseJson);
|
||||||
|
if (url.EndsWith(".json")) return Task.FromResult(ValidManifestJson);
|
||||||
|
if (url.EndsWith(".sig")) return Task.FromResult("valid-sig");
|
||||||
|
return Task.FromResult("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var verifier = new MockSignatureVerifier();
|
||||||
|
var storage = new MockFileStorage
|
||||||
|
{
|
||||||
|
OpenReadFunc = _ => new MemoryStream(Encoding.UTF8.GetBytes("wrong content"))
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = new UpdateService(http, verifier, storage);
|
||||||
|
var update = await service.CheckForUpdatesAsync();
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.DownloadAndVerifyUpdateAsync(update));
|
||||||
|
Assert.Contains("hash mismatch", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void UpdateInfo_Record_HasCorrectProperties()
|
public async Task Test4_MissingSignature_Rejected()
|
||||||
{
|
{
|
||||||
var updateInfo = new UpdateInfo("v1.0.0", "https://example.com/update.msix", "expectedhash");
|
var noSigRelease = ValidReleaseJson.Replace("{ \"name\": \"server-monitor-manager-manifest.sig\"", "//");
|
||||||
|
var http = new MockHttpTransport { GetStringAsyncFunc = _ => Task.FromResult(noSigRelease) };
|
||||||
|
var service = new UpdateService(http, new MockSignatureVerifier(), new MockFileStorage());
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.CheckForUpdatesAsync());
|
||||||
|
Assert.Contains("Manifest or signature not found", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Test5_InvalidSignature_Rejected()
|
||||||
|
{
|
||||||
|
var http = new MockHttpTransport { GetStringAsyncFunc = url => Task.FromResult(url.EndsWith("releases/latest") ? ValidReleaseJson : ValidManifestJson) };
|
||||||
|
var verifier = new MockSignatureVerifier
|
||||||
|
{
|
||||||
|
VerifySignatureAsyncFunc = (_, _) => throw new InvalidOperationException("Signature verification failed: invalid signature format")
|
||||||
|
};
|
||||||
|
var storage = new MockFileStorage();
|
||||||
|
|
||||||
|
var service = new UpdateService(http, verifier, storage);
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.CheckForUpdatesAsync());
|
||||||
|
Assert.Contains("invalid signature format", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Test8_UpdateActionNotShownUntilVerified()
|
||||||
|
{
|
||||||
|
var http = new MockHttpTransport { GetStringAsyncFunc = url => Task.FromResult(url.EndsWith("releases/latest") ? ValidReleaseJson : ValidManifestJson) };
|
||||||
|
bool signatureVerified = false;
|
||||||
|
var verifier = new MockSignatureVerifier
|
||||||
|
{
|
||||||
|
VerifySignatureAsyncFunc = (_, _) => { signatureVerified = true; return Task.CompletedTask; }
|
||||||
|
};
|
||||||
|
var storage = new MockFileStorage();
|
||||||
|
|
||||||
|
var service = new UpdateService(http, verifier, storage);
|
||||||
|
var update = await service.CheckForUpdatesAsync();
|
||||||
|
|
||||||
Assert.Equal("v1.0.0", updateInfo.Version);
|
// CheckForUpdatesAsync returns the update ONLY AFTER signature is verified
|
||||||
Assert.Equal("https://example.com/update.msix", updateInfo.DownloadUrl);
|
Assert.True(signatureVerified);
|
||||||
Assert.Equal("expectedhash", updateInfo.ExpectedHash);
|
Assert.NotNull(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Test9_PreReleaseChannel_Used()
|
||||||
|
{
|
||||||
|
var preReleaseJson = "[" + ValidReleaseJson.Replace("v0.1.0-alpha.9", "v0.1.0-alpha.10") + "]";
|
||||||
|
var preReleaseManifest = ValidManifestJson.Replace("v0.1.0-alpha.9", "v0.1.0-alpha.10");
|
||||||
|
|
||||||
|
var http = new MockHttpTransport
|
||||||
|
{
|
||||||
|
GetStringAsyncFunc = url =>
|
||||||
|
{
|
||||||
|
if (url.EndsWith("releases")) return Task.FromResult(preReleaseJson); // Note: releases array
|
||||||
|
if (url.EndsWith(".json")) return Task.FromResult(preReleaseManifest);
|
||||||
|
if (url.EndsWith(".sig")) return Task.FromResult("valid-sig");
|
||||||
|
return Task.FromResult("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var service = new UpdateService(http, new MockSignatureVerifier(), new MockFileStorage());
|
||||||
|
|
||||||
|
var update = await service.CheckForUpdatesAsync(usePreRelease: true);
|
||||||
|
Assert.Equal("v0.1.0-alpha.10", update!.Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Test10_UrlAssetFromAnotherTag_Rejected()
|
||||||
|
{
|
||||||
|
// Msix URL points to a different tag
|
||||||
|
var maliciousRelease = ValidReleaseJson.Replace("v0.1.0-alpha.9/ServerMonitorManager-win-x64.msix", "v0.1.0-alpha.8/ServerMonitorManager-win-x64.msix");
|
||||||
|
var http = new MockHttpTransport { GetStringAsyncFunc = url => Task.FromResult(url.EndsWith("releases/latest") ? maliciousRelease : ValidManifestJson) };
|
||||||
|
|
||||||
|
var service = new UpdateService(http, new MockSignatureVerifier(), new MockFileStorage());
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.CheckForUpdatesAsync());
|
||||||
|
Assert.Contains("does not match the release tag", ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ EOF
|
||||||
cosign sign-blob --yes --key cosign.key --output-signature manifest.sig manifest.json
|
cosign sign-blob --yes --key cosign.key --output-signature manifest.sig manifest.json
|
||||||
|
|
||||||
echo "Test 1: Valid signature and hash"
|
echo "Test 1: Valid signature and hash"
|
||||||
if ! bash tests/bootstrap/verify-manifest.sh "$ARCHIVE_NAME" manifest.json manifest.sig; then
|
if ! bash deploy/ochenstarik-server-monitor-manager.sh verify-manifest manifest.json manifest.sig; then
|
||||||
echo "FAIL: Valid payload rejected"
|
echo "FAIL: Valid payload rejected"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
@ -36,7 +36,8 @@ echo "PASS: Valid payload accepted"
|
||||||
|
|
||||||
echo "Test 2: Altered byte in archive"
|
echo "Test 2: Altered byte in archive"
|
||||||
echo "altered content" > "$ARCHIVE_NAME"
|
echo "altered content" > "$ARCHIVE_NAME"
|
||||||
if bash tests/bootstrap/verify-manifest.sh "$ARCHIVE_NAME" manifest.json manifest.sig >/dev/null 2>&1; then
|
# Note: we need to test archive verification for altered archive! The old test used verify-manifest which doesn't check archive.
|
||||||
|
if bash deploy/ochenstarik-server-monitor-manager.sh verify-release "$ARCHIVE_NAME" >/dev/null 2>&1; then
|
||||||
echo "FAIL: Altered archive accepted"
|
echo "FAIL: Altered archive accepted"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
@ -53,19 +54,31 @@ cat <<EOF > manifest.json
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
if bash tests/bootstrap/verify-manifest.sh "$ARCHIVE_NAME" manifest.json manifest.sig >/dev/null 2>&1; then
|
if bash deploy/ochenstarik-server-monitor-manager.sh verify-manifest manifest.json manifest.sig >/dev/null 2>&1; then
|
||||||
echo "FAIL: Substituted hash accepted"
|
echo "FAIL: Substituted hash accepted"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "PASS: Substituted hash rejected"
|
echo "PASS: Substituted hash rejected"
|
||||||
|
|
||||||
echo "Test 4: Manifest without signature"
|
echo "Test 4: Manifest without signature"
|
||||||
if bash tests/bootstrap/verify-manifest.sh "$ARCHIVE_NAME" manifest.json "" >/dev/null 2>&1; then
|
if bash deploy/ochenstarik-server-monitor-manager.sh verify-manifest manifest.json "" >/dev/null 2>&1; then
|
||||||
echo "FAIL: Missing signature accepted"
|
echo "FAIL: Missing signature accepted"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "PASS: Missing signature rejected"
|
echo "PASS: Missing signature rejected"
|
||||||
|
|
||||||
|
echo "Test 5: Real alpha.8 manifest fallback matching"
|
||||||
|
ALPHA8_ARCHIVE="ochenstarik-server-monitor-manager-linux-x64.tar.gz"
|
||||||
|
wget -qO "$ALPHA8_ARCHIVE" https://github.com/ochenstarik-ui/server-monitor-manager/releases/download/v0.1.0-alpha.8/server-monitor-manager-linux-x64.tar.gz
|
||||||
|
wget -qO server-monitor-manager-manifest.json https://github.com/ochenstarik-ui/server-monitor-manager/releases/download/v0.1.0-alpha.8/server-monitor-manager-manifest.json
|
||||||
|
wget -qO server-monitor-manager-manifest.sig https://github.com/ochenstarik-ui/server-monitor-manager/releases/download/v0.1.0-alpha.8/server-monitor-manager-manifest.sig
|
||||||
|
unset SMM_TEST_PUBKEY
|
||||||
|
if ! bash deploy/ochenstarik-server-monitor-manager.sh verify-release "$ALPHA8_ARCHIVE" >/dev/null 2>&1; then
|
||||||
|
echo "FAIL: Alpha.8 real release verification failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "PASS: Alpha.8 real release verification succeeded"
|
||||||
|
|
||||||
echo "All tests passed."
|
echo "All tests passed."
|
||||||
|
|
||||||
rm -f cosign.key cosign.pub manifest.json manifest.sig "$ARCHIVE_NAME"
|
rm -f cosign.key cosign.pub manifest.json manifest.sig "$ARCHIVE_NAME" "$ALPHA8_ARCHIVE" server-monitor-manager-manifest.json server-monitor-manager-manifest.sig
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue