返回 DeepSeek-Reasonix
verify-candidate-signing-repair.mjs
根目录 / scripts / verify-candidate-signing-repair.mjs
1 import { spawnSync } from "node:child_process";
2 import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3 import { tmpdir } from "node:os";
4 import path from "node:path";
5 import { pathToFileURL } from "node:url";
6
7 const workflowPath = ".github/workflows/release-desktop.yml";
8 const contractPath = ".signpath/contracts/release-signing.yml";
9
10 function run(command, args) {
11 const result = spawnSync(command, args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
12 if (result.status !== 0) throw new Error(`${command} ${args[0]} failed: ${result.stderr.trim() || result.stdout.trim()}`);
13 return result.stdout.trim();
14 }
15
16 export function fingerprintFiles(contract) {
17 const match = /^fingerprint_files:\n((?: - [^\n]+\n)+)/m.exec(contract);
18 if (!match) throw new Error("signing contract has no fingerprint file list");
19 const files = match[1].trimEnd().split("\n").map(line => line.slice(4));
20 if (!files.includes(workflowPath)) throw new Error("Desktop workflow is absent from the signing fingerprint");
21 return files;
22 }
23
24 export function outsideSigningContract(workflow) {
25 const startMarker = "\n signing-contract:\n";
26 const endMarker = "\n build:\n";
27 const start = workflow.indexOf(startMarker);
28 const end = workflow.indexOf(endMarker, start + startMarker.length);
29 if (start < 0 || end < 0 || workflow.indexOf(startMarker, start + 1) >= 0 || workflow.indexOf(endMarker, end + 1) >= 0) {
30 throw new Error("Desktop workflow signing-contract boundaries are ambiguous");
31 }
32 return workflow.slice(0, start) + workflow.slice(end);
33 }
34
35 export function verifyCandidateSigningRepair(candidateSHA, expectedFingerprint) {
36 if (!/^[0-9a-f]{40}$/.test(candidateSHA) || !/^v1:[0-9a-f]{64}$/.test(expectedFingerprint)) {
37 throw new Error("invalid candidate signing identity");
38 }
39 run("git", ["merge-base", "--is-ancestor", candidateSHA, "HEAD"]);
40 const contract = readFileSync(contractPath, "utf8");
41 if (spawnSync("git", ["diff", "--quiet", candidateSHA, "HEAD", "--", contractPath]).status !== 0) {
42 throw new Error("protected signing contract changed after candidate sealing");
43 }
44 const otherFiles = fingerprintFiles(contract).filter(file => file !== workflowPath);
45 if (spawnSync("git", ["diff", "--quiet", candidateSHA, "HEAD", "--", ...otherFiles]).status !== 0) {
46 throw new Error("signing inputs changed after candidate sealing");
47 }
48 const originalWorkflow = run("git", ["show", `${candidateSHA}:${workflowPath}`]);
49 if (outsideSigningContract(originalWorkflow) !== outsideSigningContract(readFileSync(workflowPath, "utf8").trimEnd())) {
50 throw new Error("Desktop build or publication changed after candidate sealing");
51 }
52
53 const temporary = mkdtempSync(path.join(tmpdir(), "reasonix-signing-candidate-"));
54 const checkout = path.join(temporary, "candidate");
55 try {
56 run("git", ["worktree", "add", "--detach", checkout, candidateSHA]);
57 const actual = run("go", ["run", "./cmd/signpath-contract", "fingerprint", checkout]);
58 if (actual !== expectedFingerprint) throw new Error("sealed candidate signing fingerprint differs from its protected control commit");
59 return actual;
60 } finally {
61 spawnSync("git", ["worktree", "remove", "--force", checkout], { encoding: "utf8" });
62 rmSync(temporary, { recursive: true, force: true });
63 }
64 }
65
66 if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
67 try {
68 process.stdout.write(`${verifyCandidateSigningRepair(process.argv[2], process.argv[3])}\n`);
69 } catch (error) {
70 process.stderr.write(`candidate signing repair: ${error.message}\n`);
71 process.exitCode = 1;
72 }
73 }
74
74 lines Plain Text