返回 DeepSeek-Reasonix
sign-macos.test.mjs
根目录 / desktop / packaging / sign-macos.test.mjs
1 import assert from "node:assert/strict";
2 import { execFileSync, spawnSync } from "node:child_process";
3 import { chmodSync, mkdirSync, mkdtempSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import { dirname, join } from "node:path";
6 import test from "node:test";
7 import { signMacOS } from "./sign-macos.mjs";
8
9 test("signing requires an explicit app and identity", async () => {
10 await assert.rejects(signMacOS("fixture.app"), /identity are required/);
11 });
12
13 test("signs both architectures of resource sidecars and framework binaries before sealing", {
14 skip: process.platform !== "darwin",
15 }, async () => {
16 const root = mkdtempSync(join(tmpdir(), "reasonix-signing-test-"));
17 try {
18 const app = join(root, "Reasonix.app");
19 const plist = (executable, type) => `<?xml version="1.0"?><plist version="1.0"><dict>
20 <key>CFBundleIdentifier</key><string>io.reasonix.fixture.${executable}</string>
21 <key>CFBundleExecutable</key><string>${executable}</string>
22 <key>CFBundlePackageType</key><string>${type}</string>
23 <key>CFBundleVersion</key><string>1</string></dict></plist>`;
24 const put = (name, contents) => {
25 mkdirSync(dirname(join(app, name)), { recursive: true });
26 writeFileSync(join(app, name), contents);
27 };
28 put("Contents/Info.plist", plist("Reasonix", "APPL"));
29 put("Contents/Frameworks/Electron Framework.framework/Versions/A/Resources/Info.plist", plist("Electron Framework", "FMWK"));
30 put("Contents/Frameworks/Squirrel.framework/Versions/A/Resources/Info.plist", plist("Squirrel", "FMWK"));
31 const binaries = [
32 "Contents/MacOS/Reasonix",
33 "Contents/Resources/service/reasonix",
34 "Contents/Resources/service/reasonix-desktop",
35 "Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework",
36 "Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libffmpeg.dylib",
37 "Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libvk_swiftshader.dylib",
38 "Contents/Frameworks/Squirrel.framework/Versions/A/Squirrel",
39 "Contents/Frameworks/Squirrel.framework/Versions/A/Resources/ShipIt",
40 ];
41 const source = join(root, "fixture.c");
42 writeFileSync(source, "int main(void) { return 0; }\n");
43 for (const relative of binaries) {
44 const file = join(app, relative);
45 mkdirSync(dirname(file), { recursive: true });
46 const dylib = relative.endsWith(".dylib") || /\/(Electron Framework|Squirrel)$/.test(relative);
47 execFileSync("clang", ["-arch", "arm64", "-arch", "x86_64", ...(dylib ? ["-dynamiclib"] : []), source, "-o", file]);
48 execFileSync("codesign", ["--remove-signature", file]);
49 if (relative.endsWith(".dylib")) chmodSync(file, 0o644);
50 }
51 symlinkSync("../Resources/service/reasonix-desktop", join(app, "Contents/MacOS/reasonix-desktop"));
52 // Valid versioned framework symlinks are essential to exercise real seals.
53 for (const name of ["Electron Framework", "Squirrel"]) {
54 const framework = join(app, "Contents/Frameworks", `${name}.framework`);
55 execFileSync("ln", ["-s", "A", join(framework, "Versions/Current")]);
56 execFileSync("ln", ["-s", `Versions/Current/${name}`, join(framework, name)]);
57 execFileSync("ln", ["-s", "Versions/Current/Resources", join(framework, "Resources")]);
58 }
59 // Reproduce the original false positive: bundle verification passes while
60 // code stored as a resource still has no signature.
61 execFileSync("codesign", ["--force", "--deep", "--options", "runtime", "-s", "-", app]);
62 execFileSync("codesign", ["--verify", "--deep", "--strict", app]);
63 assert.notEqual(spawnSync("codesign", ["--verify", join(app, binaries[2])]).status, 0);
64
65 // The control above signs MacOS siblings. Start the new signer from fully
66 // unsigned code so it must order those siblings before the main app seal.
67 for (const relative of binaries) {
68 execFileSync("codesign", ["--remove-signature", join(app, relative)]);
69 }
70
71 await signMacOS(app, "-");
72 assert.equal(readlinkSync(join(app, "Contents/MacOS/reasonix-desktop")), "../Resources/service/reasonix-desktop");
73 for (const relative of binaries) {
74 for (const arch of ["arm64", "x86_64"]) {
75 const result = spawnSync("codesign", ["--display", "--verbose=4", "--arch", arch, join(app, relative)], { encoding: "utf8" });
76 assert.equal(result.status, 0, `${relative} (${arch}): ${result.stderr}`);
77 assert.match(result.stderr, /flags=.*\(.*runtime.*\)/, `${relative} (${arch})`);
78 }
79 execFileSync("codesign", ["--verify", "--strict", "--all-architectures", join(app, relative)]);
80 }
81 execFileSync("codesign", ["--verify", "--deep", "--strict", "--all-architectures", app]);
82 } finally {
83 rmSync(root, { recursive: true, force: true });
84 }
85 });
86
86 lines Plain Text