From f866b12fbf2b316bf240f763d80b2ddc4a89b798 Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Sat, 1 Aug 2026 19:13:05 +0200 Subject: [PATCH] fix: make updater checksum verification self-contained --- README.md | 2 +- SOURCE_MANIFEST.txt | 19 ++++++++++--------- docs/RELEASE_NOTES_0.10.8.md | 9 +++++++++ package-lock.json | 4 ++-- package.json | 3 ++- scripts/apply-binary-update.ps1 | 19 +++++++++++++++++-- scripts/apply-source-update.ps1 | 13 ++++++++++++- scripts/verify.mjs | 9 +++++++-- src/renderer/mock-repository-bridge.js | 2 +- tests/update-service.test.mjs | 26 +++++++++++++++++++++++++- 10 files changed, 86 insertions(+), 20 deletions(-) create mode 100644 docs/RELEASE_NOTES_0.10.8.md diff --git a/README.md b/README.md index 0b8aa88..e766fde 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ForgeFlow is een Windows-desktopapp voor wie Git, Gitea en eigen Docker- of Unraid-servers gebruikt. Je ziet in één werkruimte wat lokaal gewijzigd is, wat op Gitea staat en welke exacte commit op de server draait. ForgeFlow begeleidt je daarna veilig door review, commit, push, deployment en verificatie. -> Huidige release: **0.10.7** · [download de laatste Windows-release](https://gitea.itworx.tech/Jens/ForgeFlow/releases/latest) +> Huidige release: **0.10.8** · [download de laatste Windows-release](https://gitea.itworx.tech/Jens/ForgeFlow/releases/latest) ![ForgeFlow release-overzicht](docs/screenshots/overview.png) diff --git a/SOURCE_MANIFEST.txt b/SOURCE_MANIFEST.txt index cc27c43..7958adc 100644 --- a/SOURCE_MANIFEST.txt +++ b/SOURCE_MANIFEST.txt @@ -1,4 +1,4 @@ -ForgeFlow 0.10.7 source manifest +ForgeFlow 0.10.8 source manifest SHA-256 BYTES PATH (The manifest excludes itself, dependencies and generated release artifacts.) cedceb71eb846d99c7c4019031833c1c7f93b84a1c6073aec7d2435dc744ca3d 703 .gitea/workflows/quality.yml @@ -40,6 +40,7 @@ f79908fb3dad98c38030c6e6be7c79a1999e0478ed9c2496923891954438daa1 4581 05ed618f5a74a854363930128ca98939808517eedd28b9a508660a0c46e91d97 884 docs/RELEASE_NOTES_0.10.5.md 94bfb2783c1befad1197e1c5e32fc002222c94a28d70d48360a8d53ecd260d5c 772 docs/RELEASE_NOTES_0.10.6.md 226a3b2d4bc7f54841749a283fcdd71b643cd585ba74d673084bee829fef6ea2 903 docs/RELEASE_NOTES_0.10.7.md +4be29ad0cb7ebcf5625172b8d2bd7a67cdc6d64d3a94e2c3f0656cdfd42dcb7a 642 docs/RELEASE_NOTES_0.10.8.md a0c00ff76acd1682bb5e0e8dcf6589c9480da436c9c6d30780a1ed58b4dad94f 1770 docs/RELEASE_NOTES_0.2.0.md 5773ead01aa4c522c556295553787482d01b1f5242f053b2c61f120c4de4fa76 5963 docs/RELEASE_NOTES_0.3.0.md d46de73cf6c4cd5c2ba3f455a7a2af2e0d64ee9d94a97fd1a0bfb44e35c1624a 1093 docs/RELEASE_NOTES_0.3.1.md @@ -101,19 +102,19 @@ c230b931abf2293d2d44b7a69b94c35f1142c093cc46b88739a0de5cbd6d1896 1532 4a561ead5ba7cdfaf4efce91842a4308c5f2a77980205879d83835efb8a579db 1067 LICENSE 1f0f388df4397e548887bbc7579fd3c864581b86469c01703201ece7a6cbf931 13667 main.cjs 91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md -df9841fba56ae74ff14800075d69b92ed24e0aca89085f47b0b3283a9321afc8 179806 package-lock.json -4be2b648fda413f2f188309691d5a68eeabcad4fa6cca52be0c9b448b425c618 5588 package.json +894cdb5af0b545aad556e7db0cc2f5c27be1cd809e951885edd8df460f7a0380 179806 package-lock.json +23e1e94840ffae0ae62d3f9fc07d5d75501699c325b8fceaaaa01ac92df6ebdd 5626 package.json 2a597a5704c576783b8a72407fbc377fa7506b36a4596ea7f7bce126e394f837 1326 playwright.config.mjs 69318fdf054be7aa2fe86ead9847da9da65745d8d5de548c8346f3ba0afc4892 12175 preload.cjs abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 PUBLISH-AND-ENABLE-UPDATE.cmd f018383f755352ca448e2ebb1e19b1dba412a3eb793d61e64b02953e300754fd 10538 Publish-ForgeFlow-Release.ps1 688fff7d2c989adb97ebb7fae38962656b70304a0aa5d27433c56adf7f136de0 4196 Publish-Missing-Binary-Release.ps1 -e4468143ec620cc5369065f1e51ef378fdcf3de4927f3280119ac01c050c3fc4 10313 README.md +539ea27276d134d92b0d34122a34f9d46491ba02aa0220f9ef93f5c90dc9661d 10313 README.md 8f8dc5ff0c2967563457e4027d795f6f515253e0a459ecca5a9f5bcf0b658870 14650 reports/architecture-audit.json 5e923f0096895f588fe66b187779cdb7cb5af6644010c6dcc4d55876930ae376 1220 reports/architecture-audit.md 509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs -8fe616851e6a089a59fd134905465019c33d26f6408ebd0613fff587b6c589e0 4400 scripts/apply-binary-update.ps1 -706b49e001837c91134044d3af132d58fb14955245bc1d5c36b413eb65867002 10486 scripts/apply-source-update.ps1 +d0745072321aca2c80f44460974a7926715a9f429164aaf7660dced40b52c736 4790 scripts/apply-binary-update.ps1 +404863bcbe7292355662e3a326455df864d7279badc29f90866a3b837420df54 10745 scripts/apply-source-update.ps1 02e924227f6cad3777fd06660230c85df590d8ce95e134194a4d18970a240b88 4145 scripts/architecture-audit.mjs 4490bed84761f76e1fd87ee3117fe53e82e60b7760329c97d68772d6d820ebae 8521 scripts/audit-installed-deployments.cjs 6d46dd6826069d842f20f9f22a99042257db936cdea0bee8d294d2d7ea290126 3893 scripts/doctor.mjs @@ -124,7 +125,7 @@ b83d443f5724ac15393567f3a688aed8315fbe3e5966832c864a9466e0669464 8102 c76507857292c5713e1c699cf02e24b80265da39af2cecd148034bdb874adbb6 5246 scripts/test-authenticode-chain.ps1 4393f7dc5f417e6d601a68238f4e26791799a3634acec228fe4d79deaee85eb5 3109 scripts/validate-installed-connections.cjs e6127e1e62f39c70ddb1abf72f4d7e7b8e3f19ff1f219e1a3660353c2e0cdfac 2411 scripts/verify-release-signatures.mjs -bc88d892ea1bb43721787fafa04d0fa2684ee8faa03e3a33d7cfc8a7984c56fe 19558 scripts/verify.mjs +b50d081c956c7ede91899c52dc26615e249d295a1b484419dcafabafb5c65a7c 19896 scripts/verify.mjs 0b9f03ba3c67ff7cdb2916a902ad8ce25e81a7c90b210e4ae52d2ad029efabf3 2353 scripts/write-release-checksums.mjs 619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1 dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412 src/main/audit-service.cjs @@ -179,7 +180,7 @@ eef2f269ba4fbb76bf66ad328d481b461255d0acb753b30878dd4d4eaac57dc6 6924 a84da5aecbb16ce7983dba1f6d6aab1bf47b2e9a87c2933fa1afb8123f7ef7d6 1497 src/renderer/index.html 06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js 5a2c0616a8a0ba23c182a8928abdf40a69612ce34c4cba47b1fd5f21fe5843d4 28617 src/renderer/mock-deployment-bridge.js -87859b1a04d39d1f5a47ce1f3f2e280367d569453f50b546d60e02a1604f216b 20032 src/renderer/mock-repository-bridge.js +c259dca30378e7462f94c736b266b7f1c97ad46a56c28ca9d5f832248ffa4fb5 20032 src/renderer/mock-repository-bridge.js 94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js 1fc4c28859f7283aa2b771a50f1fd26addc8862b97a09250f806930ab4483f7a 79874 src/renderer/styles.css 8d366214f5ce9a87911439632de1c68fe5724e564412f651190c4066f9818fac 101196 src/renderer/views.js @@ -228,7 +229,7 @@ e631e9ca49a5bac7075860aac2ff4d377a32a78377b70e06ecf833f0f192fd5f 11552 0d1bc4d623ce299337736c577ec61c8ffd6974ebe20335b72838d10eae35ecb1 7993 tests/ssh-service.test.mjs 8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs eff3cc63d2622a4924e43516b3b00bef61fa62a489995bebaee5c48b11cdf8c9 48945 tests/unraid-deployment.test.mjs -9c58c600d925db6a3ad526e661010c05619e24d61c9d19ec0e207a09ae4cb93a 23981 tests/update-service.test.mjs +bf95071f8d8bec6dcf2c889f1f9d83be1b56178dc0d2be84da555848b6319cbc 25650 tests/update-service.test.mjs 9cea5c1d5ba3e0972a0b5c7236cf1f7c5616373e0a39ea4a492ecebf70452e40 948 tests/validation.test.mjs 7ef4d4b9f5f3e6979293b29d571ce0e39f83197f3cade2d999a9cea7bacdd84d 1781 tests/zip-writer.test.mjs 8f36b542736f2933bad8b9464ad7fa37b68196009c81cf702ce3b677cd637dea 767 UPDATE_FROM_0.3.2.md diff --git a/docs/RELEASE_NOTES_0.10.8.md b/docs/RELEASE_NOTES_0.10.8.md new file mode 100644 index 0000000..587b770 --- /dev/null +++ b/docs/RELEASE_NOTES_0.10.8.md @@ -0,0 +1,9 @@ +# ForgeFlow 0.10.8 + +## Self-contained checksum verification + +- Binary and source update helpers no longer depend on the optional PowerShell `Get-FileHash` cmdlet. +- Both helpers calculate checksums directly with the built-in .NET SHA-256 implementation. +- A real Windows regression test clears `PSModulePath` and verifies the downloaded binary successfully in that minimal environment. +- The helper still validates the exact published checksum before waiting for ForgeFlow to exit or changing installed files. +- This release retains the reliable non-detached launcher and server-to-repository recognition improvements from 0.10.6 and 0.10.7. diff --git a/package-lock.json b/package-lock.json index 390745e..12108e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "forgeflow", - "version": "0.10.7", + "version": "0.10.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "forgeflow", - "version": "0.10.7", + "version": "0.10.8", "dependencies": { "ssh2": "1.17.0" }, diff --git a/package.json b/package.json index f63fefb..d5f4e01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "forgeflow", - "version": "0.10.7", + "version": "0.10.8", "private": true, "description": "Desktop release cockpit for local Git, Gitea Actions and controlled exact-commit deployments.", "main": "main.cjs", @@ -112,6 +112,7 @@ "docs/RELEASE_NOTES_0.10.5.md", "docs/RELEASE_NOTES_0.10.6.md", "docs/RELEASE_NOTES_0.10.7.md", + "docs/RELEASE_NOTES_0.10.8.md", "docs/CURRENT_STATE.md", "docs/MUTATION_MODEL.md", "docs/RELEASING.md", diff --git a/scripts/apply-binary-update.ps1 b/scripts/apply-binary-update.ps1 index 9e11d1d..bd129ef 100644 --- a/scripts/apply-binary-update.ps1 +++ b/scripts/apply-binary-update.ps1 @@ -8,7 +8,8 @@ param( [Parameter(Mandatory = $true)][string]$LogPath, [Parameter(Mandatory = $true)][string]$StatusPath, [Parameter(Mandatory = $true)][string]$UpdateId, - [switch]$HandshakeOnly + [switch]$HandshakeOnly, + [switch]$VerifyOnly ) $ErrorActionPreference = "Stop" @@ -51,6 +52,16 @@ function Write-UpdateState { function Write-Log([string]$Message) { "{0} {1}" -f [DateTime]::UtcNow.ToString("o"), $Message | Add-Content -LiteralPath $LogPath -Encoding UTF8 } +function Get-Sha256([string]$Path) { + $stream = [IO.File]::OpenRead($Path) + $algorithm = [Security.Cryptography.SHA256]::Create() + try { + return ([BitConverter]::ToString($algorithm.ComputeHash($stream))).Replace("-", "").ToLowerInvariant() + } finally { + $algorithm.Dispose() + $stream.Dispose() + } +} try { Write-UpdateState -State "started" -Message "Binary updater owns the update request." @@ -59,9 +70,13 @@ try { Write-Log "Handshake-only verification completed successfully." exit 0 } - $actualSha256 = (Get-FileHash -LiteralPath $BinaryPath -Algorithm SHA256).Hash.ToLowerInvariant() + $actualSha256 = Get-Sha256 -Path $BinaryPath if ($actualSha256 -ne $ExpectedSha256.ToLowerInvariant()) { throw "Binary update SHA-256 verification failed." } if (-not (Test-Path -LiteralPath $CurrentExecutable -PathType Leaf)) { throw "Current ForgeFlow executable was not found." } + if ($VerifyOnly) { + Write-Log "Verification-only SHA-256 check completed successfully." + exit 0 + } Write-UpdateState -State "waiting-for-exit" -Message "Waiting for ForgeFlow to close." try { Wait-Process -Id $ParentPid -Timeout 60 -ErrorAction Stop } catch { diff --git a/scripts/apply-source-update.ps1 b/scripts/apply-source-update.ps1 index e8ea425..ce442ef 100644 --- a/scripts/apply-source-update.ps1 +++ b/scripts/apply-source-update.ps1 @@ -71,6 +71,17 @@ function Write-UpdateState { } } +function Get-Sha256([string]$Path) { + $stream = [IO.File]::OpenRead($Path) + $algorithm = [Security.Cryptography.SHA256]::Create() + try { + return ([BitConverter]::ToString($algorithm.ComputeHash($stream))).Replace("-", "").ToLowerInvariant() + } finally { + $algorithm.Dispose() + $stream.Dispose() + } +} + function Invoke-Robocopy { param([string]$From, [string]$To) New-Item -ItemType Directory -Force -Path $To | Out-Null @@ -120,7 +131,7 @@ try { Start-Sleep -Milliseconds 500 } - $actualHash = (Get-FileHash -LiteralPath $ArchivePath -Algorithm SHA256).Hash.ToLowerInvariant() + $actualHash = Get-Sha256 -Path $ArchivePath if ($actualHash -ne $ExpectedSha256.ToLowerInvariant()) { throw "Update archive checksum mismatch." } $working = Join-Path ([IO.Path]::GetTempPath()) ("forgeflow-update-" + [guid]::NewGuid().ToString("N")) diff --git a/scripts/verify.mjs b/scripts/verify.mjs index cddf9f4..34fff75 100644 --- a/scripts/verify.mjs +++ b/scripts/verify.mjs @@ -87,6 +87,7 @@ const required = [ "docs/RELEASE_NOTES_0.10.5.md", "docs/RELEASE_NOTES_0.10.6.md", "docs/RELEASE_NOTES_0.10.7.md", + "docs/RELEASE_NOTES_0.10.8.md", "docs/UPDATING.md", "docs/DIAGNOSTICS.md", "docs/DEPLOYMENT_SETUP.md", @@ -125,9 +126,9 @@ for (const file of required) await access(path.join(root, file)); const packageJson = JSON.parse( await readFile(path.join(root, "package.json"), "utf8"), ); -if (packageJson.version !== "0.10.7") +if (packageJson.version !== "0.10.8") throw new Error( - `Expected package version 0.10.7, got ${packageJson.version}.`, + `Expected package version 0.10.8, got ${packageJson.version}.`, ); const sourceManifest = await readFile( path.join(root, "SOURCE_MANIFEST.txt"), @@ -469,6 +470,10 @@ const release0107 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.7.md for (const phrase of ["exact provenance", "automatic", "repository sidebar", "DevRunbook", "no container changes"]) { if (!release0107.includes(phrase)) throw new Error(`0.10.7 release notes are missing: ${phrase}`); } +const release0108 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.8.md"), "utf8"); +for (const phrase of ["Get-FileHash", ".NET SHA-256", "PSModulePath", "binary", "source update helpers"]) { + if (!release0108.includes(phrase)) throw new Error(`0.10.8 release notes are missing: ${phrase}`); +} const configSource = await readFile(path.join(root, "src/main/config-store.cjs"), "utf8"); for (const mode of ["server-git", "push-bundle", "monitor-only"]) { if (!configSource.includes(mode)) throw new Error(`Deployment configuration is missing mode: ${mode}`); diff --git a/src/renderer/mock-repository-bridge.js b/src/renderer/mock-repository-bridge.js index 1f76b28..f36ed09 100644 --- a/src/renderer/mock-repository-bridge.js +++ b/src/renderer/mock-repository-bridge.js @@ -5,7 +5,7 @@ function createMockRepositoryBridge(context) { await wait(80); snapshot(); return { - appVersion: "0.10.7-demo", + appVersion: "0.10.8-demo", platform: "win32", state: clone(state), git: { available: true, version: "git version 2.47.3" }, diff --git a/tests/update-service.test.mjs b/tests/update-service.test.mjs index d905b12..2b8f6d0 100644 --- a/tests/update-service.test.mjs +++ b/tests/update-service.test.mjs @@ -410,6 +410,29 @@ test("binary helper confirms startup through the production Node spawn options", } await rm(temp, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); }); + +test("binary helper verifies SHA-256 without PowerShell module autoloading", { skip: process.platform !== "win32" }, async () => { + const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-binary-dotnet-sha-")); + const binaryPath = path.join(temp, "update.exe"); + const currentPath = path.join(temp, "current.exe"); + const statusPath = path.join(temp, "status.json"); + const logPath = path.join(temp, "helper.log"); + const bytes = Buffer.from("verified update bytes"); + await writeFile(binaryPath, bytes); + await writeFile(currentPath, "current"); + const expectedSha256 = createHash("sha256").update(bytes).digest("hex"); + const powershell = path.join(process.env.SystemRoot || process.env.WINDIR, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"); + const scriptPath = fileURLToPath(new URL("../scripts/apply-binary-update.ps1", import.meta.url)); + const { stderr } = await execFileAsync(powershell, [ + "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath, + "-BinaryPath", binaryPath, "-ExpectedSha256", expectedSha256, "-ExpectedVersion", "9.9.9", + "-CurrentExecutable", currentPath, "-Portable", "False", "-ParentPid", String(process.pid), + "-LogPath", logPath, "-StatusPath", statusPath, "-UpdateId", "dotnet-sha", "-VerifyOnly", + ], { windowsHide: true, env: { ...process.env, PSModulePath: "" } }); + assert.equal(stderr, ""); + assert.match(await readFile(logPath, "utf8"), /Verification-only SHA-256 check completed successfully/); + await rm(temp, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); +}); test("early helper exit reports the helper log instead of only an exit code", async () => { const temp = await mkdtemp( path.join(os.tmpdir(), "forgeflow-update-log-tail-"), @@ -626,7 +649,7 @@ test("binary update helper verifies, waits, applies and records restart state", "utf8", ); for (const marker of [ - "Get-FileHash", + "Security.Cryptography.SHA256", "Wait-Process", 'Write-UpdateState -State "started"', 'Write-UpdateState -State "waiting-for-exit"', @@ -640,4 +663,5 @@ test("binary update helper verifies, waits, applies and records restart state", `missing binary updater marker: ${marker}`, ); } + assert.doesNotMatch(helper, /Get-FileHash/); });