diff --git a/SOURCE_MANIFEST.txt b/SOURCE_MANIFEST.txt index af6af69..07acd7e 100644 --- a/SOURCE_MANIFEST.txt +++ b/SOURCE_MANIFEST.txt @@ -30,7 +30,7 @@ eb42f979666e05d51c587e4223282914926a2b9b1ade9f3fb75525019ce7f738 4616 1d8aa3c688a9d330b9a5303b09e9e3049c50bf04f97807c7ff659607baa34c32 1464 docs/ERROR_CODES.md a0cd06a96f23a94e118feb012be0fa1ac51345951cb2ba8e67fb8c889c4c342a 5007 docs/LUMAOPS_SERVER_AUDIT.md 648dd6bca6b45668fb86eb3e1f6c5898dd8da0291b990f1bb5105cfd79421343 1301 docs/MUTATION_MODEL.md -0baec6f5152b332589bcdc589c2f5a32a8e3e29afd4cfe125403b77eb6c78095 3716 docs/PRODUCTION_READINESS_1.0.md +8dc95f69e6f6c8415702c8e79fb6b466c4d60061afded9140450e7e558eaefe5 3704 docs/PRODUCTION_READINESS_1.0.md f79908fb3dad98c38030c6e6be7c79a1999e0478ed9c2496923891954438daa1 4581 docs/RELEASE_AUDIT_0.6.0.md 979a0b8e129979be6b265e8571d0a3c1e9ddd4ddb6b0bf55ae748d3478e51854 2296 docs/RELEASE_NOTES_0.10.0.md a0c00ff76acd1682bb5e0e8dcf6589c9480da436c9c6d30780a1ed58b4dad94f 1770 docs/RELEASE_NOTES_0.2.0.md @@ -67,8 +67,8 @@ ed40e08bac8792f95970bc05e49bce3cc9e288a08d11565a1bd156d787360a3b 720 25169225d73d22b9d884ab3b5c1625f03fd44e53c7a7a4c4067775e80482c9f8 2182 docs/RELEASE_NOTES_0.9.3.md 720506842e0aeb30c9fc635f86a52a5545556f092e678cf37f08436243244c3d 933 docs/RELEASE_NOTES_0.9.4.md dd90c81a375f97dfb7fa8f7808db03b19d7e7dafe3818a93537397f57eaae829 2109 docs/RELEASE_NOTES_0.9.5.md -beea33f1dcaf21ef2eda1cc0fbd48cb26811d5eacdb9facf11a9153845fe2fbb 4106 docs/RELEASING.md -2b631b9d6d973bdd70869d84886ff339da351e29e17598970b3b27915674661d 4175 docs/ROADMAP.md +60cb1f1ed55322b519236dde8388ecf9ee5fd67c169fd8093b5acf61387557d8 2068 docs/RELEASING.md +ac76cb50fabde6a00f28d7e9eccd3ef1129a40665eabdc90d78690a38d424652 4195 docs/ROADMAP.md 1ccde232c060395d7aedce27e89a7647b77afe28ab71de0a5a3efeded57369d3 140415 docs/screenshots/deploy-confirmation.png b39506254ffa2c73c389fb4795b3a745368bbeb7d8514cc47a636316d6d9a6aa 107166 docs/screenshots/deployment-run.png 070e6700bdae8c628c907ba181bbf0dde0bbbbb4208f7a875503f933ff1b882e 118819 docs/screenshots/deployment-success.png @@ -76,7 +76,7 @@ b39506254ffa2c73c389fb4795b3a745368bbeb7d8514cc47a636316d6d9a6aa 107166 87546583580e8591b1306f997d27445725b0bf5a5a79a839af3a727964e65bc2 103901 docs/screenshots/git-validator.png bbdbe91679b486cc92dec4758ce1cdaf24e3277d038c57e794a04c0dee7e3a5b 84046 docs/screenshots/overview.png c8a5e80bb9fd2d442d2d23d30e6ac1528cf2330e6e19492b7c6799e2d1508b53 112868 docs/screenshots/repository-workspace.png -322624242d246d07180cc719e14c91e8fb69e123676a02e5046f4e576cca1ca1 5569 docs/SECURITY.md +158cd3a13e9c4d081a63575fbafc77e0b23812f793a15096888b3667f41fa28c 5605 docs/SECURITY.md 32a34ec13a284d3f9ceebbc107b25a844e3db096f8cafa4e43951fc2050c9a03 13552 docs/SETUP_GUIDE.md 2fd71e9bcaeb4cb10c3fa2496b7e52fedf70c5b7f871cd587e22dc060c399079 4421 docs/SSH_UNRAID_DEPLOYMENT.md b6a178215dab054006aae4944b8ffcbe7f6100691c30f08e221e3a2dbff4cd42 2147 docs/STATUS_ENDPOINT.md @@ -95,7 +95,7 @@ c230b931abf2293d2d44b7a69b94c35f1142c093cc46b88739a0de5cbd6d1896 1532 1f0f388df4397e548887bbc7579fd3c864581b86469c01703201ece7a6cbf931 13667 main.cjs 91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md bf3f507e676b3fd834240d72b945be4cd3b4018e5ebe570594eb27b8047c3969 179806 package-lock.json -8debf4523a801760c7943d5a3caea59d54add47d84c95cacac6b9fe80f8493b8 5606 package.json +aaf36269e9636f942927a73254c1881e6e8fd886a1e4d5b4b8128404a62b25d3 5321 package.json 2a597a5704c576783b8a72407fbc377fa7506b36a4596ea7f7bce126e394f837 1326 playwright.config.mjs 69318fdf054be7aa2fe86ead9847da9da65745d8d5de548c8346f3ba0afc4892 12175 preload.cjs abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 PUBLISH-AND-ENABLE-UPDATE.cmd @@ -114,11 +114,9 @@ fca922d7d1de598a0153f3828300c7d0327e0189c06a53a23d9fc33b36d45d91 4741 842436680521311594e798848b050ae4e488d0595f0de57315f6ec081c049fb9 1266 scripts/prune-dist.mjs b83d443f5724ac15393567f3a688aed8315fbe3e5966832c864a9466e0669464 8102 scripts/publish-binary-release.cjs 444b397d515d65a7ee59d3088cba869cbb812d2b8cc18fc5d255105e3edb58c2 1468 scripts/serve-demo.mjs -b547dcb3c32f1c63185c13b899730cf2d7cde6c6e439a2ae60a58b740bb33f6f 819 scripts/signed-electron-builder-config.cjs c76507857292c5713e1c699cf02e24b80265da39af2cecd148034bdb874adbb6 5246 scripts/test-authenticode-chain.ps1 4393f7dc5f417e6d601a68238f4e26791799a3634acec228fe4d79deaee85eb5 3109 scripts/validate-installed-connections.cjs -aad97c5452d35ad5f49c67124fda012ab4f89b778d0f3e9e3f553d2977ef81d0 1565 scripts/validate-signing-environment.mjs -dd1d59fde63ac1450d26c837adfb4b0ff760ef2e2817bc50fcbf40b1f404409a 2089 scripts/verify-release-signatures.mjs +50880ac76b7d681dc65019dc794efc3cea4ffd379507fe0518985312f5b39304 2096 scripts/verify-release-signatures.mjs e8c5d00737d4c5e2c37ff683e569e8d7a0510be233db3840460c78be69f2c4bc 17163 scripts/verify.mjs 0b9f03ba3c67ff7cdb2916a902ad8ce25e81a7c90b210e4ae52d2ad029efabf3 2353 scripts/write-release-checksums.mjs 619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1 @@ -223,7 +221,7 @@ e631e9ca49a5bac7075860aac2ff4d377a32a78377b70e06ecf833f0f192fd5f 11552 0d1bc4d623ce299337736c577ec61c8ffd6974ebe20335b72838d10eae35ecb1 7993 tests/ssh-service.test.mjs 8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs 4182b61e395aff310b9a964c973a43c3566df0b44c454054c3abdd9459e86e3b 49134 tests/unraid-deployment.test.mjs -603c305301eaf0955574b6eb8b393140a3d3f0eabcee558b817130e2191c72bf 19880 tests/update-service.test.mjs +c93d9706eb206b17db8ba490a1e93067f654c66595325f34532d0b1d7617fedc 19691 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/PRODUCTION_READINESS_1.0.md b/docs/PRODUCTION_READINESS_1.0.md index 1813bf3..0149f2f 100644 --- a/docs/PRODUCTION_READINESS_1.0.md +++ b/docs/PRODUCTION_READINESS_1.0.md @@ -58,22 +58,21 @@ container, repository, environment, commit parity and health distinctly. Dense inventories, long names, keyboard focus, dialogs, reduced motion and high scaling are part of the automated matrix. -## 9. Packaging, updating and signing +## 9. Packaging and updating Windows installer and portable packaging use deterministic names; old `dist` versions are pruned after every successful build. Publication stays draft until installer, portable, checksums, provenance and CycloneDX SBOM are complete. Binary -updates verify checksum, exact publisher and timestamp. A disposable local -Authenticode fixture proves installer, portable, helper and uninstaller signing, -RFC 3161 timestamping, publisher pinning and tamper rejection. +updates verify the exact release asset, executable format and published SHA-256 +before download staging and again before replacement. Authenticode is optional and +is not a release or updater dependency for this personal/internal application. ## 10. Release decision No open P0 or P1 technical issue is known after the final quality, browser, acceptance, signing and packaging gates. The technically correct status is: -`TECHNICALLY_COMPLETE_PENDING_EXTERNAL_REQUIREMENTS` +`TECHNICALLY_COMPLETE` -The sole external production dependency is: - -`PENDING_HUMAN_INPUT: trusted production Authenticode certificate and exact legal publisher subject` +There is no paid certificate or external signing-service dependency. Windows may +show its normal unknown-publisher warning during first installation. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 25e590d..0a755d0 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -10,71 +10,26 @@ npm run quality npm audit --omit=dev --audit-level=high ``` -## Signed Windows build +## Windows build — no paid services required -Production signing is fail-closed. Configure electron-builder's Authenticode -certificate through its supported CSC environment variables, then set: +ForgeFlow is a personal/internal tool. The supported release path therefore has +no certificate, Azure or other paid-service dependency: ```powershell -$env:FORGEFLOW_SIGNED_RELEASE = '1' -$env:FORGEFLOW_EXPECTED_PUBLISHER = 'CN=Exact Legal Publisher, O=Exact Legal Organization, C=BE' -npm run dist:win:signed +npm run dist:win ``` -The signed command requires signed-release mode, an exact publisher subject and -either a classic `WIN_CSC_LINK` certificate configuration or complete Azure -credentials. It enables electron-builder's `forceCodeSigning` gate, so missing -signing material cannot silently produce a production candidate. +This produces the installer and portable executable, SHA-256 sidecars, a +CycloneDX SBOM and provenance evidence. The in-app updater downloads only the +matching Gitea release asset, checks its Windows executable format and verifies +the published SHA-256 digest before staging it. The update helper verifies the +digest again immediately before replacing the installed executable. -### Recommended: Azure Artifact Signing - -1. Create an Azure Artifact Signing account and identity-validation certificate - profile for the legal ForgeFlow publisher. -2. Create an Entra app registration and give its service principal the - `Artifact Signing Certificate Profile Signer` role on that account. -3. Store the following as protected CI variables—never in Git: - -```powershell -$env:AZURE_TENANT_ID = '' -$env:AZURE_CLIENT_ID = '' -$env:AZURE_CLIENT_SECRET = '' -$env:FORGEFLOW_AZURE_SIGNING_ENDPOINT = 'https://.codesigning.azure.net/' -$env:FORGEFLOW_AZURE_SIGNING_ACCOUNT = '' -$env:FORGEFLOW_AZURE_CERTIFICATE_PROFILE = '' -$env:FORGEFLOW_SIGNED_RELEASE = '1' -$env:FORGEFLOW_EXPECTED_PUBLISHER = 'CN=' -npm run dist:win:signed -``` - -The generated configuration uses SHA-256 and Microsoft's RFC 3161 timestamp -service. `FORGEFLOW_EXPECTED_PUBLISHER` must still contain the complete subject -reported by the resulting certificate, even though Azure's builder option uses -its CN component. - -### Alternative: classic CA certificate - -When a CA supplies a CI-compatible PFX or hardware/cloud connector supported by -electron-builder, configure its protected values and use the same command: - -```powershell -$env:WIN_CSC_LINK = 'C:\secure\forgeflow-signing.pfx' -$env:WIN_CSC_KEY_PASSWORD = '' -$env:FORGEFLOW_SIGNED_RELEASE = '1' -$env:FORGEFLOW_EXPECTED_PUBLISHER = 'CN=, O=, C=BE' -npm run dist:win:signed -``` - -Do not purchase a certificate before the CA or cloud service confirms the exact -legal subject and that its key-storage method works with the intended Windows CI -runner. An ordinary OV certificate can still accumulate SmartScreen reputation; -EV or Azure Artifact Signing provides immediate publisher trust. - -Both installer and portable executable must have a valid Authenticode signature, -the expected publisher and a timestamp. The build also creates SHA-256 files, a -CycloneDX SBOM and a provenance document containing commit and build ID. - -Without `FORGEFLOW_SIGNED_RELEASE=1`, local builds may be unsigned for development -testing but must never be published as production releases. +Windows can display an `Unknown publisher` warning for an unsigned installer. +That warning concerns public publisher reputation; it does not prevent ForgeFlow +from installing or using its checksum-verified in-app updates. Authenticode can +be added later as an optional distribution convenience, but is not required for +correct operation. ## Atomic publication @@ -83,8 +38,8 @@ the installer, portable executable, two checksums, provenance and SBOM. It only publishes after all six assets are present. A failed upload leaves a draft rather than exposing an incomplete updater target. -The publisher check is an exact subject match, not a substring match. Before a -production certificate is available, validate the complete local chain with: +The optional signing acceptance fixture can still validate the complete local +Authenticode chain without purchasing or retaining a certificate: ```powershell npm run test:signing @@ -95,5 +50,5 @@ stand-ins, requires an RFC 3161 timestamp, and proves rejection of a missing timestamp, wrong publisher and a modified binary. Its certificate is removed from the current-user certificate store after the test. -The production Authenticode certificate and exact legal publisher identity are -`PENDING_HUMAN_INPUT`; all surrounding build and verification code is complete. +The disposable test certificate is removed from the current-user certificate +store after the test and is never used for a published build. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e346ac0..2753fa6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -94,7 +94,7 @@ health verification, diagnostic correlation and no arbitrary shell input. - Windows installer/portable acceptance; - macOS/Linux package validation; -- code signing, notarization and signed updates; +- optional code signing/notarization for future public distribution; - dependency/secret/package scans; - accessibility review; - hundreds-of-repositories performance tests; diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 65ae0f7..92bd0bf 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -108,9 +108,9 @@ included model uses: - atomic non-secret status JSON; - previous-SHA recording and non-zero failure exits. -## Remaining release hardening +## Optional and future release hardening -- code-sign packages and signed updates; +- optional code signing if ForgeFlow is ever distributed publicly; - validate private CA/TLS behavior in the target network; - dependency, secret and binary scans in CI; - package-level IPC/navigation regression tests; diff --git a/package.json b/package.json index 93a9076..4d0a01c 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "coverage": "c8 --check-coverage --lines 75 --functions 75 --branches 65 --statements 75 node --test tests/*.test.mjs", "verify": "node scripts/verify.mjs", "dist:win": "electron-builder --win nsis portable && node scripts/write-release-checksums.mjs && node scripts/verify-release-signatures.mjs && node scripts/prune-dist.mjs", - "dist:win:signed": "node scripts/validate-signing-environment.mjs && electron-builder --config scripts/signed-electron-builder-config.cjs --win nsis portable && node scripts/write-release-checksums.mjs && node scripts/verify-release-signatures.mjs && node scripts/prune-dist.mjs", "dist:linux": "electron-builder --linux AppImage && node scripts/prune-dist.mjs", "dist:mac": "electron-builder --mac dmg && node scripts/prune-dist.mjs", "doctor": "node scripts/doctor.mjs", diff --git a/scripts/signed-electron-builder-config.cjs b/scripts/signed-electron-builder-config.cjs deleted file mode 100644 index f1adc2b..0000000 --- a/scripts/signed-electron-builder-config.cjs +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; - -const pkg = require("../package.json"); - -const expectedPublisher = String(process.env.FORGEFLOW_EXPECTED_PUBLISHER || "").trim(); -const commonName = expectedPublisher.match(/^CN=([^,]+)/i)?.[1]?.trim(); -const useAzure = Boolean(String(process.env.FORGEFLOW_AZURE_SIGNING_ENDPOINT || "").trim()); -const win = { ...pkg.build.win, forceCodeSigning: true }; - -if (useAzure) { - win.azureSignOptions = { - publisherName: commonName, - endpoint: process.env.FORGEFLOW_AZURE_SIGNING_ENDPOINT, - codeSigningAccountName: process.env.FORGEFLOW_AZURE_SIGNING_ACCOUNT, - certificateProfileName: process.env.FORGEFLOW_AZURE_CERTIFICATE_PROFILE, - fileDigest: "SHA256", - timestampDigest: "SHA256", - timestampRfc3161: "http://timestamp.acs.microsoft.com", - }; -} - -module.exports = { ...pkg.build, win }; diff --git a/scripts/validate-signing-environment.mjs b/scripts/validate-signing-environment.mjs deleted file mode 100644 index 24be4c2..0000000 --- a/scripts/validate-signing-environment.mjs +++ /dev/null @@ -1,17 +0,0 @@ -const signedRelease = process.env.FORGEFLOW_SIGNED_RELEASE === "1"; -const publisher = String(process.env.FORGEFLOW_EXPECTED_PUBLISHER || "").trim(); -const classicCertificate = String(process.env.WIN_CSC_LINK || process.env.CSC_LINK || "").trim(); -const azureNames = ["AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "FORGEFLOW_AZURE_SIGNING_ENDPOINT", "FORGEFLOW_AZURE_SIGNING_ACCOUNT", "FORGEFLOW_AZURE_CERTIFICATE_PROFILE"]; -const azureValues = azureNames.map((name) => String(process.env[name] || "").trim()); -const azure = azureValues.every(Boolean); -const partialAzure = azureValues.some(Boolean) && !azure; - -if (!signedRelease) throw new Error("FORGEFLOW_SIGNED_RELEASE=1 is required for the production signing build."); -if (!/^CN=.+/i.test(publisher)) throw new Error("FORGEFLOW_EXPECTED_PUBLISHER must be the exact certificate subject beginning with CN=."); -if (partialAzure) throw new Error(`Azure Artifact Signing is incomplete. Configure: ${azureNames.filter((_, index) => !azureValues[index]).join(", ")}.`); -if (!classicCertificate && !azure) throw new Error("Configure WIN_CSC_LINK/CSC_LINK or all Azure Artifact Signing credentials before building a signed release."); -if (classicCertificate && !String(process.env.WIN_CSC_KEY_PASSWORD || process.env.CSC_KEY_PASSWORD || "").trim()) { - throw new Error("WIN_CSC_KEY_PASSWORD or CSC_KEY_PASSWORD is required for classic certificate signing."); -} - -console.log(`Production ${azure ? "Azure Artifact Signing" : "classic certificate"} environment accepted for exact publisher ${publisher}.`); diff --git a/scripts/verify-release-signatures.mjs b/scripts/verify-release-signatures.mjs index 57c8edd..a4594a9 100644 --- a/scripts/verify-release-signatures.mjs +++ b/scripts/verify-release-signatures.mjs @@ -19,5 +19,5 @@ for (const artifact of artifacts) { const valid = result.Status === "Valid" && Boolean(result.TimestampSubject); const publisherMatches = !expectedPublisher || String(result.Subject || "").trim() === expectedPublisher; if (signedRelease && (!valid || !publisherMatches)) throw new Error(`Signed release verification failed for ${path.basename(artifact)}: status=${result.Status}, publisher=${result.Subject || "missing"}, timestamp=${result.TimestampSubject || "missing"}.`); - console.log(`${path.basename(artifact)}: ${valid && publisherMatches ? "valid signed artifact" : "unsigned development artifact"}`); + console.log(`${path.basename(artifact)}: ${valid && publisherMatches ? "valid signed artifact" : "checksum-protected unsigned artifact"}`); } diff --git a/tests/update-service.test.mjs b/tests/update-service.test.mjs index c542920..7531a1b 100644 --- a/tests/update-service.test.mjs +++ b/tests/update-service.test.mjs @@ -507,7 +507,7 @@ test("packaged updater rejects a binary whose checksum does not match", async () await rm(temp, { recursive: true, force: true }); }); -test("Windows release pipeline fails closed on signatures and emits provenance plus SBOM", async () => { +test("Windows release pipeline preserves optional signing checks and emits provenance plus SBOM", async () => { const [pkgSource, signatureSource, checksumSource] = await Promise.all([ readFile(new URL("../package.json", import.meta.url), "utf8"), readFile(new URL("../scripts/verify-release-signatures.mjs", import.meta.url), "utf8"), @@ -528,20 +528,17 @@ test("Windows release pipeline fails closed on signatures and emits provenance p assert.match(publisher, /sbom\.cdx\.json/); }); -test("production signing build supports classic and Azure identities but always fails closed", async () => { - const [pkg, validator, signedConfig] = await Promise.all([ +test("the supported Windows build is free, checksum-protected and updater-compatible", async () => { + const [pkg, signatureCheck, checksumWriter] = await Promise.all([ readFile(new URL("../package.json", import.meta.url), "utf8"), - readFile(new URL("../scripts/validate-signing-environment.mjs", import.meta.url), "utf8"), - readFile(new URL("../scripts/signed-electron-builder-config.cjs", import.meta.url), "utf8"), + readFile(new URL("../scripts/verify-release-signatures.mjs", import.meta.url), "utf8"), + readFile(new URL("../scripts/write-release-checksums.mjs", import.meta.url), "utf8"), ]); - assert.match(pkg, /dist:win:signed/); - assert.match(validator, /FORGEFLOW_SIGNED_RELEASE/); - assert.match(validator, /WIN_CSC_LINK/); - assert.match(validator, /FORGEFLOW_AZURE_CERTIFICATE_PROFILE/); - assert.match(validator, /exact certificate subject/); - assert.match(signedConfig, /forceCodeSigning:\s*true/); - assert.match(signedConfig, /azureSignOptions/); - assert.match(signedConfig, /timestamp\.acs\.microsoft\.com/); + assert.doesNotMatch(pkg, /dist:win:signed/); + assert.match(pkg, /dist:win/); + assert.match(pkg, /write-release-checksums\.mjs/); + assert.match(signatureCheck, /checksum-protected unsigned artifact/); + assert.match(checksumWriter, /sha256/); }); test("binary update helper verifies, waits, applies and records restart state", async () => {