From 0cfab263b713ce935da7531fffa2fa6ddaacf064 Mon Sep 17 00:00:00 2001 From: Daniel Petro <45949032+WellDunDun@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:04:55 +0300 Subject: [PATCH] fix(desktop): verify signed native runtime libraries --- .changeset/signed-native-runtime.md | 5 ++ apps/desktop/scripts/build-sidecar.ts | 9 ++-- apps/desktop/src/main/runtime-install.test.ts | 49 +++++++++++++++++-- apps/desktop/src/main/runtime-integrity.ts | 30 ++++++++++-- 4 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 .changeset/signed-native-runtime.md diff --git a/.changeset/signed-native-runtime.md b/.changeset/signed-native-runtime.md new file mode 100644 index 00000000..8e678d9c --- /dev/null +++ b/.changeset/signed-native-runtime.md @@ -0,0 +1,5 @@ +--- +"@selftune/desktop": patch +--- + +Fix signed macOS Desktop startup by accounting for code-signing changes to the bundled DuckDB native libraries. Keep signed-bundle verification, exact hashes for other runtime files, and byte-identical installed runtime checks. diff --git a/apps/desktop/scripts/build-sidecar.ts b/apps/desktop/scripts/build-sidecar.ts index f942497b..904b17c8 100644 --- a/apps/desktop/scripts/build-sidecar.ts +++ b/apps/desktop/scripts/build-sidecar.ts @@ -11,6 +11,7 @@ import { writeFile, } from "node:fs/promises"; import { dirname, join, relative, resolve, sep } from "node:path"; +import { isSigningMutableRuntimePath } from "../src/main/runtime-integrity"; const desktopRoot = resolve(import.meta.dir, ".."); const selfTuneRoot = resolve(desktopRoot, "../.."); @@ -50,14 +51,16 @@ async function listFiles(directory: string): Promise { return nested.flat(); } -async function writeRuntimeManifest(executable: string): Promise { +async function writeRuntimeManifest(): Promise { const files = await Promise.all( (await listFiles(resourceRoot)).map(async (path) => { const contents = await readFile(path); const info = await stat(path); return { path: relative(resourceRoot, path).split(sep).join("/"), - signing_mutable: relative(resourceRoot, path).split(sep).join("/") === executable, + signing_mutable: isSigningMutableRuntimePath( + relative(resourceRoot, path).split(sep).join("/"), + ), sha256: createHash("sha256").update(contents).digest("hex"), size: info.size, }; @@ -209,6 +212,6 @@ await cp( join(selfTuneRoot, "skill/settings_snippet.json"), join(resourceRoot, "settings_snippet.json"), ); -await writeRuntimeManifest(executable); +await writeRuntimeManifest(); process.stdout.write(`Staged the SelfTune runtime and dashboard at ${resourceRoot}\n`); diff --git a/apps/desktop/src/main/runtime-install.test.ts b/apps/desktop/src/main/runtime-install.test.ts index 88734279..7697be5f 100644 --- a/apps/desktop/src/main/runtime-install.test.ts +++ b/apps/desktop/src/main/runtime-install.test.ts @@ -1,8 +1,8 @@ import { afterEach, describe, expect, it } from "bun:test"; import { createHash } from "node:crypto"; -import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { parseDeveloperIdSigningIdentity, @@ -16,18 +16,32 @@ afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); -function runtimeFixture(): string { +function runtimeFixture(nativePath?: string): string { const root = mkdtempSync(join(tmpdir(), "selftune-runtime-install-")); roots.push(root); const executable = Buffer.from("selftune-runtime"); const settings = Buffer.from('{"version":1}\n'); writeFileSync(join(root, "selftune"), executable, { mode: 0o700 }); writeFileSync(join(root, "settings_snippet.json"), settings); + if (nativePath) { + mkdirSync(dirname(join(root, nativePath)), { recursive: true }); + writeFileSync(join(root, nativePath), "unsigned-native-library", { mode: 0o700 }); + } writeFileSync( join(root, "runtime-manifest.json"), JSON.stringify({ version: 2, files: [ + ...(nativePath + ? [ + { + path: nativePath, + signing_mutable: true, + size: Buffer.byteLength("unsigned-native-library"), + sha256: createHash("sha256").update("unsigned-native-library").digest("hex"), + }, + ] + : []), { path: "selftune", signing_mutable: true, @@ -156,4 +170,33 @@ describe("stable desktop runtime integrity", () => { writeFileSync(join(root, "settings_snippet.json"), "tampered"); expect(verifyRuntimeDirectory(root, { allowPlatformSigningMutation: true })).toBeFalse(); }); + + for (const path of [ + "selftune-report-worker", + "selftune-report-worker.exe", + "node_modules/@duckdb/node-api/node_modules/@duckdb/node-bindings/native/duckdb.node", + "node_modules/@duckdb/node-api/node_modules/@duckdb/node-bindings/native/libduckdb.dylib", + ]) { + it(`accepts signing changes to ${path} only with verified source trust and identical copies`, () => { + const source = runtimeFixture(path); + const candidate = runtimeFixture(path); + expect(verifyRuntimeDirectory(source)).toBeTrue(); + writeFileSync(join(source, path), "developer-id-signed-library"); + writeFileSync(join(candidate, path), "developer-id-signed-library"); + expect(verifyRuntimeDirectory(source)).toBeFalse(); + expect(verifyRuntimeDirectory(source, { allowPlatformSigningMutation: true })).toBeTrue(); + expect( + runtimeMatchesSignedSource(source, candidate, { allowPlatformSigningMutation: true }), + ).toBeTrue(); + writeFileSync(join(candidate, path), "tampered-library"); + expect( + runtimeMatchesSignedSource(source, candidate, { allowPlatformSigningMutation: true }), + ).toBeFalse(); + }); + } + + it("rejects a manifest that marks ordinary code as signing mutable", () => { + const root = runtimeFixture("node_modules/untrusted/index.js"); + expect(verifyRuntimeDirectory(root, { allowPlatformSigningMutation: true })).toBeFalse(); + }); }); diff --git a/apps/desktop/src/main/runtime-integrity.ts b/apps/desktop/src/main/runtime-integrity.ts index 9420bdc3..5304145f 100644 --- a/apps/desktop/src/main/runtime-integrity.ts +++ b/apps/desktop/src/main/runtime-integrity.ts @@ -20,6 +20,25 @@ export interface RuntimeIntegrityOptions { readonly allowPlatformSigningMutation?: boolean; } +function isRuntimeExecutable(path: string): boolean { + return [ + "selftune", + "selftune.exe", + "selftune-report-worker", + "selftune-report-worker.exe", + ].includes(path); +} + +export function isSigningMutableRuntimePath(path: string): boolean { + return ( + isRuntimeExecutable(path) || + path === + "node_modules/@duckdb/node-api/node_modules/@duckdb/node-bindings/native/duckdb.node" || + path === + "node_modules/@duckdb/node-api/node_modules/@duckdb/node-bindings/native/libduckdb.dylib" + ); +} + export interface DeveloperIdSigningIdentity { readonly authority: string; readonly teamIdentifier: string; @@ -52,8 +71,9 @@ export function verifyRuntimeDirectory( if (manifest.files.length === 0) return false; const signingMutable = manifest.files.filter((entry) => entry.signing_mutable); if ( - signingMutable.length !== 1 || - !["selftune", "selftune.exe"].includes(signingMutable[0]?.path ?? "") + signingMutable.filter((entry) => ["selftune", "selftune.exe"].includes(entry.path)).length !== + 1 || + signingMutable.some((entry) => !isSigningMutableRuntimePath(entry.path)) ) { return false; } @@ -70,7 +90,11 @@ export function verifyRuntimeDirectory( if (!existsSync(path)) return false; const info = statSync(path); if (!info.isFile()) return false; - if (entry.signing_mutable && process.platform !== "win32" && (info.mode & 0o111) === 0) { + if ( + isRuntimeExecutable(entry.path) && + process.platform !== "win32" && + (info.mode & 0o111) === 0 + ) { return false; } const matchesBuildHash = info.size === entry.size && sha256(path) === entry.sha256;