diff --git a/tests/ServerMonitorManager.Desktop.Security.Tests/UpdateServiceTests.cs b/tests/ServerMonitorManager.Desktop.Security.Tests/UpdateServiceTests.cs index c00e650..3825d75 100644 --- a/tests/ServerMonitorManager.Desktop.Security.Tests/UpdateServiceTests.cs +++ b/tests/ServerMonitorManager.Desktop.Security.Tests/UpdateServiceTests.cs @@ -49,6 +49,16 @@ namespace ServerMonitorManager.Desktop.Security.Tests ] }"; + // Release JSON without the .sig asset — used by Test4 + private const string NoSigReleaseJson = @" + { + ""tag_name"": ""v0.1.0-alpha.9"", + ""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"": ""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"", @@ -130,8 +140,7 @@ namespace ServerMonitorManager.Desktop.Security.Tests [Fact] public async Task Test4_MissingSignature_Rejected() { - var noSigRelease = ValidReleaseJson.Replace("{ \"name\": \"server-monitor-manager-manifest.sig\"", "//"); - var http = new MockHttpTransport { GetStringAsyncFunc = _ => Task.FromResult(noSigRelease) }; + var http = new MockHttpTransport { GetStringAsyncFunc = _ => Task.FromResult(NoSigReleaseJson) }; var service = new UpdateService(http, new MockSignatureVerifier(), new MockFileStorage()); var ex = await Assert.ThrowsAsync(() => service.CheckForUpdatesAsync()); @@ -154,7 +163,7 @@ namespace ServerMonitorManager.Desktop.Security.Tests } [Fact] - public async Task Test8_UpdateActionNotShownUntilVerified() + public async Task Test6_UpdateActionNotShownUntilVerified() { var http = new MockHttpTransport { GetStringAsyncFunc = url => Task.FromResult(url.EndsWith("releases/latest") ? ValidReleaseJson : ValidManifestJson) }; bool signatureVerified = false; @@ -173,7 +182,7 @@ namespace ServerMonitorManager.Desktop.Security.Tests } [Fact] - public async Task Test9_PreReleaseChannel_Used() + public async Task Test7_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"); @@ -195,7 +204,7 @@ namespace ServerMonitorManager.Desktop.Security.Tests } [Fact] - public async Task Test10_UrlAssetFromAnotherTag_Rejected() + public async Task Test8_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"); @@ -206,5 +215,54 @@ namespace ServerMonitorManager.Desktop.Security.Tests var ex = await Assert.ThrowsAsync(() => service.CheckForUpdatesAsync()); Assert.Contains("does not match the release tag", ex.Message); } + + [Fact] + public async Task Test9_ManifestVersionMismatch_Rejected() + { + // Manifest says v0.1.0-alpha.8 but release tag says v0.1.0-alpha.9 + var mismatchedManifest = ValidManifestJson.Replace("v0.1.0-alpha.9", "v0.1.0-alpha.8"); + var http = new MockHttpTransport + { + GetStringAsyncFunc = url => + { + if (url.EndsWith("releases/latest")) return Task.FromResult(ValidReleaseJson); + if (url.EndsWith(".json")) return Task.FromResult(mismatchedManifest); + if (url.EndsWith(".sig")) return Task.FromResult("valid-sig"); + return Task.FromResult(""); + } + }; + var service = new UpdateService(http, new MockSignatureVerifier(), new MockFileStorage()); + + var ex = await Assert.ThrowsAsync(() => service.CheckForUpdatesAsync()); + Assert.Contains("version", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task Test10_MissingMsixAsset_Rejected() + { + // Release JSON without the MSIX asset + var noMsixRelease = @" + { + ""tag_name"": ""v0.1.0-alpha.9"", + ""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"" } + ] + }"; + var http = new MockHttpTransport + { + GetStringAsyncFunc = url => + { + if (url.EndsWith("releases/latest")) return Task.FromResult(noMsixRelease); + if (url.EndsWith(".json")) return Task.FromResult(ValidManifestJson); + if (url.EndsWith(".sig")) return Task.FromResult("valid-sig"); + return Task.FromResult(""); + } + }; + var service = new UpdateService(http, new MockSignatureVerifier(), new MockFileStorage()); + + var ex = await Assert.ThrowsAsync(() => service.CheckForUpdatesAsync()); + Assert.Contains("MSIX", ex.Message, StringComparison.OrdinalIgnoreCase); + } } } diff --git a/tests/bootstrap/test-manifest-verification.sh b/tests/bootstrap/test-manifest-verification.sh index 583d0c2..535f0c7 100644 --- a/tests/bootstrap/test-manifest-verification.sh +++ b/tests/bootstrap/test-manifest-verification.sh @@ -1,6 +1,12 @@ #!/usr/bin/env bash set -Eeuo pipefail +CLEANUP_FILES=() +cleanup() { + rm -f "${CLEANUP_FILES[@]}" +} +trap cleanup EXIT + echo "Running negative tests for manifest verification..." if ! command -v cosign &> /dev/null; then @@ -12,12 +18,15 @@ fi export COSIGN_PASSWORD="" cosign generate-key-pair export SMM_TEST_PUBKEY="cosign.pub" +CLEANUP_FILES+=(cosign.key cosign.pub) ARCHIVE_NAME="test-archive.tar.gz" echo "archive content" > "$ARCHIVE_NAME" ARCHIVE_HASH=$(sha256sum "$ARCHIVE_NAME" | awk '{print $1}') +CLEANUP_FILES+=("$ARCHIVE_NAME") -cat < manifest.json +# Use the canonical manifest name that verify_archive() expects +cat < server-monitor-manager-manifest.json { "hashes": { "$ARCHIVE_NAME": "$ARCHIVE_HASH" @@ -25,10 +34,11 @@ cat < manifest.json } EOF -cosign sign-blob --yes --key cosign.key --output-signature manifest.sig manifest.json +cosign sign-blob --yes --key cosign.key --output-signature server-monitor-manager-manifest.sig server-monitor-manager-manifest.json +CLEANUP_FILES+=(server-monitor-manager-manifest.json server-monitor-manager-manifest.sig) echo "Test 1: Valid signature and hash" -if ! bash deploy/ochenstarik-server-monitor-manager.sh verify-manifest manifest.json manifest.sig; then +if ! bash deploy/ochenstarik-server-monitor-manager.sh verify-manifest server-monitor-manager-manifest.json server-monitor-manager-manifest.sig; then echo "FAIL: Valid payload rejected" exit 1 fi @@ -36,7 +46,8 @@ echo "PASS: Valid payload accepted" echo "Test 2: Altered byte in archive" echo "altered content" > "$ARCHIVE_NAME" -# Note: we need to test archive verification for altered archive! The old test used verify-manifest which doesn't check archive. +# verify-release will call verify_archive → verify_manifest (signature check) then sha256 (hash check). +# The manifest was signed with the original hash, so the archive hash won't match. if bash deploy/ochenstarik-server-monitor-manager.sh verify-release "$ARCHIVE_NAME" >/dev/null 2>&1; then echo "FAIL: Altered archive accepted" exit 1 @@ -46,39 +57,42 @@ echo "PASS: Altered archive rejected" echo "Test 3: Substituted hash in manifest without resigning" # Restore archive echo "archive content" > "$ARCHIVE_NAME" -# Corrupt manifest -cat < manifest.json +# Corrupt manifest (but don't re-sign — signature should now be invalid) +cat < server-monitor-manager-manifest.json { "hashes": { "$ARCHIVE_NAME": "0000000000000000000000000000000000000000000000000000000000000000" } } EOF -if bash deploy/ochenstarik-server-monitor-manager.sh verify-manifest manifest.json manifest.sig >/dev/null 2>&1; then +if bash deploy/ochenstarik-server-monitor-manager.sh verify-manifest server-monitor-manager-manifest.json server-monitor-manager-manifest.sig >/dev/null 2>&1; then echo "FAIL: Substituted hash accepted" exit 1 fi echo "PASS: Substituted hash rejected" echo "Test 4: Manifest without signature" -if bash deploy/ochenstarik-server-monitor-manager.sh verify-manifest manifest.json "" >/dev/null 2>&1; then +if bash deploy/ochenstarik-server-monitor-manager.sh verify-manifest server-monitor-manager-manifest.json "" >/dev/null 2>&1; then echo "FAIL: Missing signature accepted" exit 1 fi echo "PASS: Missing signature rejected" -echo "Test 5: Real alpha.8 manifest fallback matching" +echo "Test 5: Real alpha.8 manifest fallback matching (REQUIRES_NETWORK)" 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 +if ! 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; then + echo "SKIP: Could not download alpha.8 archive (network unavailable)" +else + 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 + CLEANUP_FILES+=("$ALPHA8_ARCHIVE") + # Use keyless verification against real Sigstore/Rekor (requires network) + 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" fi -echo "PASS: Alpha.8 real release verification succeeded" echo "All tests passed." - -rm -f cosign.key cosign.pub manifest.json manifest.sig "$ARCHIVE_NAME" "$ALPHA8_ARCHIVE" server-monitor-manager-manifest.json server-monitor-manager-manifest.sig