feat: add safe Gitea sync and signed updates
ForgeFlow quality gate / secret-scan (push) Failing after 32s
ForgeFlow quality gate / quality (push) Failing after 0s

This commit is contained in:
NuklearRabbit
2026-08-27 00:38:58 +02:00
parent cb9bdcd713
commit d47c7b5e41
46 changed files with 1658 additions and 246 deletions
+124 -24
View File
@@ -5,7 +5,7 @@ import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import { EventEmitter } from "node:events";
import { createHash } from "node:crypto";
import { createHash, generateKeyPairSync, sign } from "node:crypto";
import { execFile, spawn } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
@@ -14,10 +14,36 @@ const require = createRequire(import.meta.url);
const execFileAsync = promisify(execFile);
const {
UpdateService,
verifyReleaseManifest,
waitForUpdaterStarted,
windowsUpdaterSpawnOptions,
} = require("../src/main/update-service.cjs");
function createSignedReleaseFixture({
version,
remoteSha,
assetName,
binary,
}) {
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
const sha256 = createHash("sha256").update(binary).digest("hex");
const manifest = {
schemaVersion: 1,
product: "ForgeFlow",
version,
tag: `v${version}`,
commit: remoteSha,
buildId: "test-build",
signature: { algorithm: "Ed25519", keyId: "SHA256:test" },
artifacts: [{ name: assetName, bytes: binary.length, sha256 }],
};
const manifestBytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
const signatureBytes = Buffer.from(
`${sign(null, manifestBytes, privateKey).toString("base64")}\n`,
);
return { publicKey, sha256, manifestBytes, signatureBytes };
}
test("Windows updater uses a hidden non-detached PowerShell child", () => {
assert.deepEqual(windowsUpdaterSpawnOptions("C:\\updates"), {
detached: false,
@@ -324,6 +350,8 @@ test("packaged updater passes Gitea browser download URLs to the asset downloade
);
assert.match(source, /downloadUrl: asset\.browser_download_url/);
assert.match(source, /downloadUrl: checksumAsset\.browser_download_url/);
assert.match(source, /downloadUrl: manifestAsset\.browser_download_url/);
assert.match(source, /downloadUrl: signatureAsset\.browser_download_url/);
assert.match(source, /RELEASE_ASSET_METADATA_RECEIVED/);
});
test("PowerShell helper replaces an existing launching status with a Windows-safe file API", async () => {
@@ -480,15 +508,22 @@ test("updater handshake rejects a stale status from another update request", asy
await rm(temp, { recursive: true, force: true });
});
test("packaged updater downloads only a published checksum-matched Windows asset", async () => {
test("packaged updater downloads only a publisher-signed Windows asset", async () => {
const temp = await mkdtemp(
path.join(os.tmpdir(), "forgeflow-binary-update-"),
);
const binary = Buffer.alloc(1_100_000, 0x5a);
binary[0] = 0x4d;
binary[1] = 0x5a;
const sha256 = createHash("sha256").update(binary).digest("hex");
const assetName = "ForgeFlow-Setup-0.8.2-win-x64.exe";
const remoteSha = "a".repeat(40);
const signed = createSignedReleaseFixture({
version: "0.8.2",
remoteSha,
assetName,
binary,
});
const manifestName = "ForgeFlow-0.8.2-release-manifest.json";
const gitea = {
async getReleaseByTag(_owner, _repo, tag) {
if (tag !== "v0.8.2") return null;
@@ -508,18 +543,32 @@ test("packaged updater downloads only a published checksum-matched Windows asset
id: 42,
browser_download_url: "http://wrong-origin.test/checksum",
},
{
name: manifestName,
id: 43,
browser_download_url: "http://wrong-origin.test/manifest",
},
{
name: `${manifestName}.sig`,
id: 44,
browser_download_url: "http://wrong-origin.test/signature",
},
],
};
},
async downloadReleaseAsset(_owner, _repo, releaseId, assetId, options) {
assert.equal(releaseId, 82);
assert.equal(
options.downloadUrl,
assetId === 42
? "http://wrong-origin.test/checksum"
: "http://wrong-origin.test/setup",
);
return assetId === 42 ? Buffer.from(`${sha256} ${assetName}\n`) : binary;
const downloads = {
41: ["http://wrong-origin.test/setup", binary],
42: [
"http://wrong-origin.test/checksum",
Buffer.from(`${signed.sha256} ${assetName}\n`),
],
43: ["http://wrong-origin.test/manifest", signed.manifestBytes],
44: ["http://wrong-origin.test/signature", signed.signatureBytes],
};
assert.equal(options.downloadUrl, downloads[assetId][0]);
return downloads[assetId][1];
},
};
const service = new UpdateService({
@@ -537,14 +586,17 @@ test("packaged updater downloads only a published checksum-matched Windows asset
sourcePath: temp,
userDataPath: temp,
platform: "win32",
updatePublicKey: signed.publicKey,
});
const result = await service.downloadPackaged({
owner: "Jens",
repo: "ForgeFlow",
remoteVersion: "0.8.2",
remoteSha,
});
assert.equal(result.downloaded, true);
assert.equal(result.sha256, sha256);
assert.equal(result.sha256, signed.sha256);
assert.equal(result.publisherKeyId, "SHA256:test");
assert.equal(result.portable, false);
assert.equal((await readFile(result.binaryPath)).length, binary.length);
await rm(temp, { recursive: true, force: true });
@@ -558,6 +610,14 @@ test("packaged updater rejects a binary whose checksum does not match", async ()
binary[0] = 0x4d;
binary[1] = 0x5a;
const assetName = "ForgeFlow-Portable-0.8.2-win-x64.exe";
const remoteSha = "b".repeat(40);
const signed = createSignedReleaseFixture({
version: "0.8.2",
remoteSha,
assetName,
binary,
});
const manifestName = "ForgeFlow-0.8.2-release-manifest.json";
const service = new UpdateService({
store: { data: { gitea: {} }, save: async () => {} },
gitea: {
@@ -576,14 +636,18 @@ test("packaged updater rejects a binary whose checksum does not match", async ()
name: `${assetName}.sha256`,
browser_download_url: "http://wrong-origin.test/checksum",
},
{ id: 53, name: manifestName },
{ id: 54, name: `${manifestName}.sig` },
],
};
},
async downloadReleaseAsset(_owner, _repo, releaseId, assetId) {
assert.equal(releaseId, 83);
return assetId === 52
? Buffer.from(`${"0".repeat(64)} ${assetName}`)
: binary;
if (assetId === 51) return binary;
if (assetId === 52)
return Buffer.from(`${"0".repeat(64)} ${assetName}`);
if (assetId === 53) return signed.manifestBytes;
return signed.signatureBytes;
},
},
diagnostics: null,
@@ -595,6 +659,7 @@ test("packaged updater rejects a binary whose checksum does not match", async ()
sourcePath: temp,
userDataPath: temp,
platform: "win32",
updatePublicKey: signed.publicKey,
});
await assert.rejects(
() =>
@@ -602,17 +667,45 @@ test("packaged updater rejects a binary whose checksum does not match", async ()
owner: "Jens",
repo: "ForgeFlow",
remoteVersion: "0.8.2",
remoteSha,
}),
/SHA-256 verification/,
/does not match the signed publisher manifest/,
);
await rm(temp, { recursive: true, force: true });
});
test("Windows release pipeline preserves optional signing checks and emits provenance plus SBOM", async () => {
const [pkgSource, signatureSource, checksumSource] = await Promise.all([
test("release manifest verification rejects a different publisher key", () => {
const binary = Buffer.alloc(1_100_000, 0x5a);
const assetName = "ForgeFlow-Setup-0.8.2-win-x64.exe";
const fixture = createSignedReleaseFixture({
version: "0.8.2",
remoteSha: "c".repeat(40),
assetName,
binary,
});
const otherKey = generateKeyPairSync("ed25519").publicKey;
assert.throws(
() =>
verifyReleaseManifest({
manifestBytes: fixture.manifestBytes,
signatureBytes: fixture.signatureBytes,
publicKey: otherKey,
update: {
remoteVersion: "0.8.2",
remoteSha: "c".repeat(40),
},
assetName,
}),
(error) => error.code === "RELEASE_SIGNATURE_INVALID",
);
});
test("Windows release pipeline emits signed provenance, manifest and SBOM evidence", async () => {
const [pkgSource, signatureSource, checksumSource, manifestSigner] = await Promise.all([
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/write-release-checksums.mjs", import.meta.url), "utf8"),
readFile(new URL("../scripts/sign-release-manifest.mjs", import.meta.url), "utf8"),
]);
assert.match(pkgSource, /verify-release-signatures\.mjs/);
assert.match(signatureSource, /FORGEFLOW_SIGNED_RELEASE/);
@@ -623,6 +716,10 @@ test("Windows release pipeline preserves optional signing checks and emits prove
assert.match(checksumSource, /provenance\.json/);
assert.match(checksumSource, /sbom\.cdx\.json/);
assert.match(checksumSource, /CycloneDX/);
assert.match(checksumSource, /publisherManifestSignature/);
assert.match(manifestSigner, /Ed25519/);
assert.match(manifestSigner, /release-manifest\.json/);
assert.match(pkgSource, /sign-release-manifest\.mjs/);
const publisher = await readFile(new URL("../scripts/publish-binary-release.cjs", import.meta.url), "utf8");
assert.match(publisher, /draft: true/);
assert.match(publisher, /requiredAssets/);
@@ -630,17 +727,20 @@ test("Windows release pipeline preserves optional signing checks and emits prove
assert.match(publisher, /sbom\.cdx\.json/);
});
test("the supported Windows build is free, checksum-protected and updater-compatible", async () => {
const [pkg, signatureCheck, checksumWriter] = await Promise.all([
test("the supported Windows build uses free offline Ed25519 publisher signing", async () => {
const [pkg, keySetup, manifestSigner, publicKey] = await Promise.all([
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/write-release-checksums.mjs", import.meta.url), "utf8"),
readFile(new URL("../scripts/setup-update-signing-key.mjs", import.meta.url), "utf8"),
readFile(new URL("../scripts/sign-release-manifest.mjs", import.meta.url), "utf8"),
readFile(new URL("../build/update-signing-public.pem", import.meta.url), "utf8"),
]);
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/);
assert.match(pkg, /signing:setup/);
assert.match(keySetup, /release-signing-private\.pem/);
assert.match(manifestSigner, /sign\(null, manifestBytes, privateKey\)/);
assert.match(publicKey, /BEGIN PUBLIC KEY/);
assert.doesNotMatch(publicKey, /PRIVATE KEY/);
});
test("binary update helper verifies, waits, applies and records restart state", async () => {