fix: make packaged updates certificate-free
ForgeFlow quality gate / quality (push) Canceled after 0s

This commit is contained in:
NuklearRabbit
2026-07-29 23:11:24 +02:00
parent 18f42621c2
commit 6ef4620388
10 changed files with 46 additions and 137 deletions
+7 -9
View File
@@ -30,7 +30,7 @@ eb42f979666e05d51c587e4223282914926a2b9b1ade9f3fb75525019ce7f738 4616
1d8aa3c688a9d330b9a5303b09e9e3049c50bf04f97807c7ff659607baa34c32 1464 docs/ERROR_CODES.md 1d8aa3c688a9d330b9a5303b09e9e3049c50bf04f97807c7ff659607baa34c32 1464 docs/ERROR_CODES.md
a0cd06a96f23a94e118feb012be0fa1ac51345951cb2ba8e67fb8c889c4c342a 5007 docs/LUMAOPS_SERVER_AUDIT.md a0cd06a96f23a94e118feb012be0fa1ac51345951cb2ba8e67fb8c889c4c342a 5007 docs/LUMAOPS_SERVER_AUDIT.md
648dd6bca6b45668fb86eb3e1f6c5898dd8da0291b990f1bb5105cfd79421343 1301 docs/MUTATION_MODEL.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 f79908fb3dad98c38030c6e6be7c79a1999e0478ed9c2496923891954438daa1 4581 docs/RELEASE_AUDIT_0.6.0.md
979a0b8e129979be6b265e8571d0a3c1e9ddd4ddb6b0bf55ae748d3478e51854 2296 docs/RELEASE_NOTES_0.10.0.md 979a0b8e129979be6b265e8571d0a3c1e9ddd4ddb6b0bf55ae748d3478e51854 2296 docs/RELEASE_NOTES_0.10.0.md
a0c00ff76acd1682bb5e0e8dcf6589c9480da436c9c6d30780a1ed58b4dad94f 1770 docs/RELEASE_NOTES_0.2.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 25169225d73d22b9d884ab3b5c1625f03fd44e53c7a7a4c4067775e80482c9f8 2182 docs/RELEASE_NOTES_0.9.3.md
720506842e0aeb30c9fc635f86a52a5545556f092e678cf37f08436243244c3d 933 docs/RELEASE_NOTES_0.9.4.md 720506842e0aeb30c9fc635f86a52a5545556f092e678cf37f08436243244c3d 933 docs/RELEASE_NOTES_0.9.4.md
dd90c81a375f97dfb7fa8f7808db03b19d7e7dafe3818a93537397f57eaae829 2109 docs/RELEASE_NOTES_0.9.5.md dd90c81a375f97dfb7fa8f7808db03b19d7e7dafe3818a93537397f57eaae829 2109 docs/RELEASE_NOTES_0.9.5.md
beea33f1dcaf21ef2eda1cc0fbd48cb26811d5eacdb9facf11a9153845fe2fbb 4106 docs/RELEASING.md 60cb1f1ed55322b519236dde8388ecf9ee5fd67c169fd8093b5acf61387557d8 2068 docs/RELEASING.md
2b631b9d6d973bdd70869d84886ff339da351e29e17598970b3b27915674661d 4175 docs/ROADMAP.md ac76cb50fabde6a00f28d7e9eccd3ef1129a40665eabdc90d78690a38d424652 4195 docs/ROADMAP.md
1ccde232c060395d7aedce27e89a7647b77afe28ab71de0a5a3efeded57369d3 140415 docs/screenshots/deploy-confirmation.png 1ccde232c060395d7aedce27e89a7647b77afe28ab71de0a5a3efeded57369d3 140415 docs/screenshots/deploy-confirmation.png
b39506254ffa2c73c389fb4795b3a745368bbeb7d8514cc47a636316d6d9a6aa 107166 docs/screenshots/deployment-run.png b39506254ffa2c73c389fb4795b3a745368bbeb7d8514cc47a636316d6d9a6aa 107166 docs/screenshots/deployment-run.png
070e6700bdae8c628c907ba181bbf0dde0bbbbb4208f7a875503f933ff1b882e 118819 docs/screenshots/deployment-success.png 070e6700bdae8c628c907ba181bbf0dde0bbbbb4208f7a875503f933ff1b882e 118819 docs/screenshots/deployment-success.png
@@ -76,7 +76,7 @@ b39506254ffa2c73c389fb4795b3a745368bbeb7d8514cc47a636316d6d9a6aa 107166
87546583580e8591b1306f997d27445725b0bf5a5a79a839af3a727964e65bc2 103901 docs/screenshots/git-validator.png 87546583580e8591b1306f997d27445725b0bf5a5a79a839af3a727964e65bc2 103901 docs/screenshots/git-validator.png
bbdbe91679b486cc92dec4758ce1cdaf24e3277d038c57e794a04c0dee7e3a5b 84046 docs/screenshots/overview.png bbdbe91679b486cc92dec4758ce1cdaf24e3277d038c57e794a04c0dee7e3a5b 84046 docs/screenshots/overview.png
c8a5e80bb9fd2d442d2d23d30e6ac1528cf2330e6e19492b7c6799e2d1508b53 112868 docs/screenshots/repository-workspace.png c8a5e80bb9fd2d442d2d23d30e6ac1528cf2330e6e19492b7c6799e2d1508b53 112868 docs/screenshots/repository-workspace.png
322624242d246d07180cc719e14c91e8fb69e123676a02e5046f4e576cca1ca1 5569 docs/SECURITY.md 158cd3a13e9c4d081a63575fbafc77e0b23812f793a15096888b3667f41fa28c 5605 docs/SECURITY.md
32a34ec13a284d3f9ceebbc107b25a844e3db096f8cafa4e43951fc2050c9a03 13552 docs/SETUP_GUIDE.md 32a34ec13a284d3f9ceebbc107b25a844e3db096f8cafa4e43951fc2050c9a03 13552 docs/SETUP_GUIDE.md
2fd71e9bcaeb4cb10c3fa2496b7e52fedf70c5b7f871cd587e22dc060c399079 4421 docs/SSH_UNRAID_DEPLOYMENT.md 2fd71e9bcaeb4cb10c3fa2496b7e52fedf70c5b7f871cd587e22dc060c399079 4421 docs/SSH_UNRAID_DEPLOYMENT.md
b6a178215dab054006aae4944b8ffcbe7f6100691c30f08e221e3a2dbff4cd42 2147 docs/STATUS_ENDPOINT.md b6a178215dab054006aae4944b8ffcbe7f6100691c30f08e221e3a2dbff4cd42 2147 docs/STATUS_ENDPOINT.md
@@ -95,7 +95,7 @@ c230b931abf2293d2d44b7a69b94c35f1142c093cc46b88739a0de5cbd6d1896 1532
1f0f388df4397e548887bbc7579fd3c864581b86469c01703201ece7a6cbf931 13667 main.cjs 1f0f388df4397e548887bbc7579fd3c864581b86469c01703201ece7a6cbf931 13667 main.cjs
91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md 91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md
bf3f507e676b3fd834240d72b945be4cd3b4018e5ebe570594eb27b8047c3969 179806 package-lock.json bf3f507e676b3fd834240d72b945be4cd3b4018e5ebe570594eb27b8047c3969 179806 package-lock.json
8debf4523a801760c7943d5a3caea59d54add47d84c95cacac6b9fe80f8493b8 5606 package.json aaf36269e9636f942927a73254c1881e6e8fd886a1e4d5b4b8128404a62b25d3 5321 package.json
2a597a5704c576783b8a72407fbc377fa7506b36a4596ea7f7bce126e394f837 1326 playwright.config.mjs 2a597a5704c576783b8a72407fbc377fa7506b36a4596ea7f7bce126e394f837 1326 playwright.config.mjs
69318fdf054be7aa2fe86ead9847da9da65745d8d5de548c8346f3ba0afc4892 12175 preload.cjs 69318fdf054be7aa2fe86ead9847da9da65745d8d5de548c8346f3ba0afc4892 12175 preload.cjs
abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 PUBLISH-AND-ENABLE-UPDATE.cmd abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 PUBLISH-AND-ENABLE-UPDATE.cmd
@@ -114,11 +114,9 @@ fca922d7d1de598a0153f3828300c7d0327e0189c06a53a23d9fc33b36d45d91 4741
842436680521311594e798848b050ae4e488d0595f0de57315f6ec081c049fb9 1266 scripts/prune-dist.mjs 842436680521311594e798848b050ae4e488d0595f0de57315f6ec081c049fb9 1266 scripts/prune-dist.mjs
b83d443f5724ac15393567f3a688aed8315fbe3e5966832c864a9466e0669464 8102 scripts/publish-binary-release.cjs b83d443f5724ac15393567f3a688aed8315fbe3e5966832c864a9466e0669464 8102 scripts/publish-binary-release.cjs
444b397d515d65a7ee59d3088cba869cbb812d2b8cc18fc5d255105e3edb58c2 1468 scripts/serve-demo.mjs 444b397d515d65a7ee59d3088cba869cbb812d2b8cc18fc5d255105e3edb58c2 1468 scripts/serve-demo.mjs
b547dcb3c32f1c63185c13b899730cf2d7cde6c6e439a2ae60a58b740bb33f6f 819 scripts/signed-electron-builder-config.cjs
c76507857292c5713e1c699cf02e24b80265da39af2cecd148034bdb874adbb6 5246 scripts/test-authenticode-chain.ps1 c76507857292c5713e1c699cf02e24b80265da39af2cecd148034bdb874adbb6 5246 scripts/test-authenticode-chain.ps1
4393f7dc5f417e6d601a68238f4e26791799a3634acec228fe4d79deaee85eb5 3109 scripts/validate-installed-connections.cjs 4393f7dc5f417e6d601a68238f4e26791799a3634acec228fe4d79deaee85eb5 3109 scripts/validate-installed-connections.cjs
aad97c5452d35ad5f49c67124fda012ab4f89b778d0f3e9e3f553d2977ef81d0 1565 scripts/validate-signing-environment.mjs 50880ac76b7d681dc65019dc794efc3cea4ffd379507fe0518985312f5b39304 2096 scripts/verify-release-signatures.mjs
dd1d59fde63ac1450d26c837adfb4b0ff760ef2e2817bc50fcbf40b1f404409a 2089 scripts/verify-release-signatures.mjs
e8c5d00737d4c5e2c37ff683e569e8d7a0510be233db3840460c78be69f2c4bc 17163 scripts/verify.mjs e8c5d00737d4c5e2c37ff683e569e8d7a0510be233db3840460c78be69f2c4bc 17163 scripts/verify.mjs
0b9f03ba3c67ff7cdb2916a902ad8ce25e81a7c90b210e4ae52d2ad029efabf3 2353 scripts/write-release-checksums.mjs 0b9f03ba3c67ff7cdb2916a902ad8ce25e81a7c90b210e4ae52d2ad029efabf3 2353 scripts/write-release-checksums.mjs
619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1 619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1
@@ -223,7 +221,7 @@ e631e9ca49a5bac7075860aac2ff4d377a32a78377b70e06ecf833f0f192fd5f 11552
0d1bc4d623ce299337736c577ec61c8ffd6974ebe20335b72838d10eae35ecb1 7993 tests/ssh-service.test.mjs 0d1bc4d623ce299337736c577ec61c8ffd6974ebe20335b72838d10eae35ecb1 7993 tests/ssh-service.test.mjs
8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs 8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs
4182b61e395aff310b9a964c973a43c3566df0b44c454054c3abdd9459e86e3b 49134 tests/unraid-deployment.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 9cea5c1d5ba3e0972a0b5c7236cf1f7c5616373e0a39ea4a492ecebf70452e40 948 tests/validation.test.mjs
7ef4d4b9f5f3e6979293b29d571ce0e39f83197f3cade2d999a9cea7bacdd84d 1781 tests/zip-writer.test.mjs 7ef4d4b9f5f3e6979293b29d571ce0e39f83197f3cade2d999a9cea7bacdd84d 1781 tests/zip-writer.test.mjs
8f36b542736f2933bad8b9464ad7fa37b68196009c81cf702ce3b677cd637dea 767 UPDATE_FROM_0.3.2.md 8f36b542736f2933bad8b9464ad7fa37b68196009c81cf702ce3b677cd637dea 767 UPDATE_FROM_0.3.2.md
+7 -8
View File
@@ -58,22 +58,21 @@ container, repository, environment, commit parity and health distinctly. Dense
inventories, long names, keyboard focus, dialogs, reduced motion and high scaling inventories, long names, keyboard focus, dialogs, reduced motion and high scaling
are part of the automated matrix. 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` Windows installer and portable packaging use deterministic names; old `dist`
versions are pruned after every successful build. Publication stays draft until versions are pruned after every successful build. Publication stays draft until
installer, portable, checksums, provenance and CycloneDX SBOM are complete. Binary installer, portable, checksums, provenance and CycloneDX SBOM are complete. Binary
updates verify checksum, exact publisher and timestamp. A disposable local updates verify the exact release asset, executable format and published SHA-256
Authenticode fixture proves installer, portable, helper and uninstaller signing, before download staging and again before replacement. Authenticode is optional and
RFC 3161 timestamping, publisher pinning and tamper rejection. is not a release or updater dependency for this personal/internal application.
## 10. Release decision ## 10. Release decision
No open P0 or P1 technical issue is known after the final quality, browser, No open P0 or P1 technical issue is known after the final quality, browser,
acceptance, signing and packaging gates. The technically correct status is: acceptance, signing and packaging gates. The technically correct status is:
`TECHNICALLY_COMPLETE_PENDING_EXTERNAL_REQUIREMENTS` `TECHNICALLY_COMPLETE`
The sole external production dependency is: There is no paid certificate or external signing-service dependency. Windows may
show its normal unknown-publisher warning during first installation.
`PENDING_HUMAN_INPUT: trusted production Authenticode certificate and exact legal publisher subject`
+18 -63
View File
@@ -10,71 +10,26 @@ npm run quality
npm audit --omit=dev --audit-level=high 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 ForgeFlow is a personal/internal tool. The supported release path therefore has
certificate through its supported CSC environment variables, then set: no certificate, Azure or other paid-service dependency:
```powershell ```powershell
$env:FORGEFLOW_SIGNED_RELEASE = '1' npm run dist:win
$env:FORGEFLOW_EXPECTED_PUBLISHER = 'CN=Exact Legal Publisher, O=Exact Legal Organization, C=BE'
npm run dist:win:signed
``` ```
The signed command requires signed-release mode, an exact publisher subject and This produces the installer and portable executable, SHA-256 sidecars, a
either a classic `WIN_CSC_LINK` certificate configuration or complete Azure CycloneDX SBOM and provenance evidence. The in-app updater downloads only the
credentials. It enables electron-builder's `forceCodeSigning` gate, so missing matching Gitea release asset, checks its Windows executable format and verifies
signing material cannot silently produce a production candidate. 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 Windows can display an `Unknown publisher` warning for an unsigned installer.
That warning concerns public publisher reputation; it does not prevent ForgeFlow
1. Create an Azure Artifact Signing account and identity-validation certificate from installing or using its checksum-verified in-app updates. Authenticode can
profile for the legal ForgeFlow publisher. be added later as an optional distribution convenience, but is not required for
2. Create an Entra app registration and give its service principal the correct operation.
`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 = '<tenant id>'
$env:AZURE_CLIENT_ID = '<application/client id>'
$env:AZURE_CLIENT_SECRET = '<secret value>'
$env:FORGEFLOW_AZURE_SIGNING_ENDPOINT = 'https://<region>.codesigning.azure.net/'
$env:FORGEFLOW_AZURE_SIGNING_ACCOUNT = '<artifact signing account>'
$env:FORGEFLOW_AZURE_CERTIFICATE_PROFILE = '<certificate profile>'
$env:FORGEFLOW_SIGNED_RELEASE = '1'
$env:FORGEFLOW_EXPECTED_PUBLISHER = 'CN=<exact subject from Get-AuthenticodeSignature>'
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 = '<secret password>'
$env:FORGEFLOW_SIGNED_RELEASE = '1'
$env:FORGEFLOW_EXPECTED_PUBLISHER = 'CN=<exact legal subject>, O=<organization>, 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.
## Atomic publication ## 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 publishes after all six assets are present. A failed upload leaves a draft rather
than exposing an incomplete updater target. than exposing an incomplete updater target.
The publisher check is an exact subject match, not a substring match. Before a The optional signing acceptance fixture can still validate the complete local
production certificate is available, validate the complete local chain with: Authenticode chain without purchasing or retaining a certificate:
```powershell ```powershell
npm run test:signing 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 timestamp, wrong publisher and a modified binary. Its certificate is removed
from the current-user certificate store after the test. from the current-user certificate store after the test.
The production Authenticode certificate and exact legal publisher identity are The disposable test certificate is removed from the current-user certificate
`PENDING_HUMAN_INPUT`; all surrounding build and verification code is complete. store after the test and is never used for a published build.
+1 -1
View File
@@ -94,7 +94,7 @@ health verification, diagnostic correlation and no arbitrary shell input.
- Windows installer/portable acceptance; - Windows installer/portable acceptance;
- macOS/Linux package validation; - macOS/Linux package validation;
- code signing, notarization and signed updates; - optional code signing/notarization for future public distribution;
- dependency/secret/package scans; - dependency/secret/package scans;
- accessibility review; - accessibility review;
- hundreds-of-repositories performance tests; - hundreds-of-repositories performance tests;
+2 -2
View File
@@ -108,9 +108,9 @@ included model uses:
- atomic non-secret status JSON; - atomic non-secret status JSON;
- previous-SHA recording and non-zero failure exits. - 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; - validate private CA/TLS behavior in the target network;
- dependency, secret and binary scans in CI; - dependency, secret and binary scans in CI;
- package-level IPC/navigation regression tests; - package-level IPC/navigation regression tests;
-1
View File
@@ -14,7 +14,6 @@
"coverage": "c8 --check-coverage --lines 75 --functions 75 --branches 65 --statements 75 node --test tests/*.test.mjs", "coverage": "c8 --check-coverage --lines 75 --functions 75 --branches 65 --statements 75 node --test tests/*.test.mjs",
"verify": "node scripts/verify.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": "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:linux": "electron-builder --linux AppImage && node scripts/prune-dist.mjs",
"dist:mac": "electron-builder --mac dmg && node scripts/prune-dist.mjs", "dist:mac": "electron-builder --mac dmg && node scripts/prune-dist.mjs",
"doctor": "node scripts/doctor.mjs", "doctor": "node scripts/doctor.mjs",
@@ -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 };
-17
View File
@@ -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}.`);
+1 -1
View File
@@ -19,5 +19,5 @@ for (const artifact of artifacts) {
const valid = result.Status === "Valid" && Boolean(result.TimestampSubject); const valid = result.Status === "Valid" && Boolean(result.TimestampSubject);
const publisherMatches = !expectedPublisher || String(result.Subject || "").trim() === expectedPublisher; 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"}.`); 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"}`);
} }
+10 -13
View File
@@ -507,7 +507,7 @@ test("packaged updater rejects a binary whose checksum does not match", async ()
await rm(temp, { recursive: true, force: true }); 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([ const [pkgSource, signatureSource, checksumSource] = await Promise.all([
readFile(new URL("../package.json", import.meta.url), "utf8"), readFile(new URL("../package.json", import.meta.url), "utf8"),
readFile(new URL("../scripts/verify-release-signatures.mjs", 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/); assert.match(publisher, /sbom\.cdx\.json/);
}); });
test("production signing build supports classic and Azure identities but always fails closed", async () => { test("the supported Windows build is free, checksum-protected and updater-compatible", async () => {
const [pkg, validator, signedConfig] = await Promise.all([ const [pkg, signatureCheck, checksumWriter] = await Promise.all([
readFile(new URL("../package.json", import.meta.url), "utf8"), 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/verify-release-signatures.mjs", import.meta.url), "utf8"),
readFile(new URL("../scripts/signed-electron-builder-config.cjs", import.meta.url), "utf8"), readFile(new URL("../scripts/write-release-checksums.mjs", import.meta.url), "utf8"),
]); ]);
assert.match(pkg, /dist:win:signed/); assert.doesNotMatch(pkg, /dist:win:signed/);
assert.match(validator, /FORGEFLOW_SIGNED_RELEASE/); assert.match(pkg, /dist:win/);
assert.match(validator, /WIN_CSC_LINK/); assert.match(pkg, /write-release-checksums\.mjs/);
assert.match(validator, /FORGEFLOW_AZURE_CERTIFICATE_PROFILE/); assert.match(signatureCheck, /checksum-protected unsigned artifact/);
assert.match(validator, /exact certificate subject/); assert.match(checksumWriter, /sha256/);
assert.match(signedConfig, /forceCodeSigning:\s*true/);
assert.match(signedConfig, /azureSignOptions/);
assert.match(signedConfig, /timestamp\.acs\.microsoft\.com/);
}); });
test("binary update helper verifies, waits, applies and records restart state", async () => { test("binary update helper verifies, waits, applies and records restart state", async () => {