import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; import { accessSync, constants, lstatSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { createRequire } from "node:module"; import { LICENSE_FILES } from "./cua-linux-release.mjs"; import { CLOUDFLARED_ASSETS, CLOUDFLARED_VERSION, executableTarget, } from "./prepare-cloudflared.mjs"; const require = createRequire(import.meta.url); const { validateDriverCandidate } = require("../electron/cua-linux.cjs"); const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const releaseDir = path.resolve(process.argv[2] ?? path.join(root, "release")); function fail(message) { throw new Error(`[verify-linux-package] ${message}`); } function exactlyOne(suffix) { const matches = readdirSync(releaseDir) .filter((name) => name.endsWith(suffix)) .map((name) => path.join(releaseDir, name)); if (matches.length !== 1) fail(`expected exactly one ${suffix} artifact, found ${matches.length}`); return matches[0]; } function requireFile(file) { if (!statSync(file, { throwIfNoEntry: false })?.isFile()) fail(`missing file: ${file}`); } function requireExecutable(file) { requireFile(file); try { accessSync(file, constants.X_OK); } catch { fail(`not executable: ${file}`); } } // electron-updater picks its installer from resources/package-type ("deb" → // DebUpdater, absent → AppImageUpdater), and the app's updater reads the same // marker; a marker in both artifacts (or neither) silently inverts the routing. function requirePackageType(resources, label, expected) { const marker = path.join(resources, "package-type"); const found = statSync(marker, { throwIfNoEntry: false })?.isFile() ? readFileSync(marker, "utf8").trim() : null; if (found !== expected) { fail( `${label} package-type marker is ${JSON.stringify(found)}, expected ${JSON.stringify(expected)}`, ); } } function requireUpdaterTarget(resources, label) { const updateFile = path.join(resources, "app-update.yml"); requireFile(updateFile); const update = readFileSync(updateFile, "utf8"); if (!/^owner: milind-soni$/m.test(update) || !/^repo: OpenMausBot$/m.test(update)) { fail(`${label} app-update.yml does not point at milind-soni/OpenMausBot`); } } function sha256(file) { return createHash("sha256").update(readFileSync(file)).digest("hex"); } function requireRegularMode(file, mode) { const details = lstatSync(file, { throwIfNoEntry: false }); if (!details?.isFile() || details.isSymbolicLink()) fail(`expected a regular file: ${file}`); if ((details.mode & 0o777) !== mode) { fail(`expected mode ${mode.toString(8)} for ${file}, found ${(details.mode & 0o777).toString(8)}`); } } function requireDirectoryMode(directory, mode) { const details = lstatSync(directory, { throwIfNoEntry: false }); if (!details?.isDirectory() || details.isSymbolicLink()) { fail(`expected a real directory: ${directory}`); } if ((details.mode & 0o777) !== mode) { fail( `expected mode ${mode.toString(8)} for ${directory}, found ${(details.mode & 0o777).toString(8)}`, ); } } function requireExactEntries(directory, expected) { const actual = readdirSync(directory).sort(); const wanted = [...expected].sort(); if (JSON.stringify(actual) !== JSON.stringify(wanted)) { fail(`${directory} entries differ: expected ${wanted.join(", ")}; found ${actual.join(", ")}`); } } function requireContained(root, target) { const canonicalRoot = realpathSync(root); const canonicalTarget = realpathSync(target); if (canonicalTarget !== canonicalRoot && !canonicalTarget.startsWith(`${canonicalRoot}${path.sep}`)) { fail(`${target} resolves outside ${root}`); } } function verifyCompliance(licenses, label) { const notices = readFileSync(path.join(licenses, "THIRD_PARTY_NOTICES.md"), "utf8"); const html = readFileSync(path.join(licenses, "THIRD_PARTY_LICENSES.html"), "utf8"); const sbom = JSON.parse(readFileSync(path.join(licenses, "SBOM.cdx.json"), "utf8")); const components = sbom.components ?? []; if (components.length !== 392) { fail(`${label} SBOM must contain 381 registry packages, 10 Cua packages, and Inter`); } const componentRefs = new Set(); for (const component of components) { const reference = component["bom-ref"]; if (typeof reference !== "string" || reference.length === 0) { fail(`${label} SBOM component has no bom-ref: ${component.name ?? "unknown"}`); } if (componentRefs.has(reference)) fail(`${label} SBOM repeats bom-ref ${reference}`); componentRefs.add(reference); } const registry = components.filter((component) => component.group === "crates.io"); const trycua = components.filter((component) => component.group === "trycua"); const fonts = components.filter((component) => component.group === "rsms"); if (registry.length !== 381 || trycua.length !== 10 || fonts.length !== 1) { fail(`${label} SBOM component groups do not match the reviewed runtime graph`); } const trycuaNames = trycua.map((component) => component.name).sort(); if ( JSON.stringify(trycuaNames) !== JSON.stringify([ "cua-cursor-motion", "cua-driver", "cua-driver-contract", "cua-driver-core", "cua-driver-sdk", "cua-telemetry", "cursor-overlay", "cursor-theme-cli", "pip-preview", "platform-linux", ]) ) { fail(`${label} SBOM Cua workspace allowlist changed`); } if (fonts[0]?.name !== "Inter" || fonts[0]?.version !== "4.001") { fail(`${label} SBOM is missing the reviewed Inter font component`); } const registryIds = new Set(); for (const component of registry) { const packageId = component.properties?.find( (property) => property.name === "openmausbot:cargo:package-id", )?.value; if (typeof packageId !== "string" || !packageId.startsWith("registry+")) { fail(`${label} SBOM registry component has no exact Cargo package ID`); } if (registryIds.has(packageId)) fail(`${label} SBOM repeats ${packageId}`); registryIds.add(packageId); if (!component.hashes?.some((hash) => hash.alg === "SHA-256" && /^[a-f0-9]{64}$/.test(hash.content))) { fail(`${label} SBOM registry component has no Cargo.lock checksum: ${component.name}`); } if (!notices.includes(`| ${component.name.replaceAll("|", "\\|")} | ${component.version} |`)) { fail(`${label} notices are missing SBOM attribution for ${component.name}@${component.version}`); } } const attributed = new Set( [...html.matchAll(//g)].map((match) => match[1]), ); if (attributed.size !== 381) { fail(`${label} license report does not cover the reviewed 381 registry packages`); } for (const packageId of attributed) { if (!registryIds.has(packageId)) { fail(`${label} license report contains an unknown Cargo package: ${packageId}`); } } const mpls = components .filter((component) => JSON.stringify(component.licenses).includes("MPL-2.0")) .map((component) => `${component.name}@${component.version}`) .sort(); if ( JSON.stringify(mpls) !== JSON.stringify([ "option-ext@0.2.0", "uniffi@0.31.0", "uniffi_core@0.31.0", "uniffi_internal_macros@0.31.0", "uniffi_macros@0.31.0", "uniffi_meta@0.31.0", "uniffi_pipeline@0.31.0", ]) ) { fail(`${label} MPL component set changed`); } const rootDependency = sbom.dependencies?.find( (dependency) => dependency.ref === "pkg:generic/cua-driver-linux-x64@0.41.3", ); const rootReferences = rootDependency?.dependsOn; const uniqueRootReferences = new Set(rootReferences ?? []); if ( !Array.isArray(rootReferences) || rootReferences.length !== componentRefs.size || uniqueRootReferences.size !== rootReferences.length || [...uniqueRootReferences].some((reference) => !componentRefs.has(reference)) ) { fail(`${label} SBOM root must reference every reviewed component`); } for (const expected of [ "Mozilla Public License 2.0", "SIL Open Font License 1.1", "bumpalo@3.20.2", "typed-path@0.12.3", "zip@8.6.0", "zlib-rs@0.6.6", "zopfli@0.8.3", ]) { if (!(html + notices.replaceAll(" | ", "@")).includes(expected)) { fail(`${label} compliance records are missing ${expected}`); } } } function verifyCuaResources(resources, label, { directoryMode = 0o755, validateRuntimePath = true, } = {}) { requireDirectoryMode(resources, directoryMode); const cuaRoot = path.join(resources, "cua-linux-x64"); const licenses = path.join(cuaRoot, "licenses"); requireDirectoryMode(cuaRoot, directoryMode); requireDirectoryMode(licenses, directoryMode); requireExactEntries(cuaRoot, ["cua-driver", "cua-cursor-theme", "licenses", "release.json"]); requireExactEntries(licenses, [ "LICENSE.md", "Inter-OFL-1.1.txt", "THIRD_PARTY_LICENSES.html", "THIRD_PARTY_NOTICES.md", "SBOM.cdx.json", ]); requireContained(resources, cuaRoot); requireContained(cuaRoot, licenses); const driver = path.join(cuaRoot, "cua-driver"); const cursorTheme = path.join(cuaRoot, "cua-cursor-theme"); requireRegularMode(driver, 0o755); requireRegularMode(cursorTheme, 0o755); requireContained(cuaRoot, driver); requireContained(cuaRoot, cursorTheme); const expectedHashes = new Map([ [driver, "83030028e7c9e3f4d753b3b27025135d44eb8d52f562cce0aaaf95777bc1b41f"], [cursorTheme, "d39efb960d207e7328d7a9dd48e3b6565f74be073f9c2ca8dcba86a4d0f28312"], ]); for (const [file, expected] of expectedHashes) { const actual = sha256(file); if (actual !== expected) fail(`${label} has the wrong hash for ${path.basename(file)}: ${actual}`); } const commandEnvironment = { LANG: "C", LC_ALL: "C", CUA_DRIVER_RS_UPDATE_CHECK: "false", CUA_DRIVER_RS_TELEMETRY_ENABLED: "false", }; const version = execFileSync(driver, ["--version"], { encoding: "utf8", env: commandEnvironment, timeout: 5_000, }).trim(); if (version !== "cua-driver 0.41.3") fail(`${label} CUA version is ${JSON.stringify(version)}`); const manifest = JSON.parse( execFileSync(driver, ["manifest"], { encoding: "utf8", env: commandEnvironment, timeout: 5_000, maxBuffer: 256 * 1024, }), ); const invocationCommand = manifest.mcp_invocation?.command; let invocationPath = null; if (typeof invocationCommand === "string" && invocationCommand.length > 0) { try { invocationPath = realpathSync(invocationCommand); } catch {} } if ( manifest.schema_version !== "1" || manifest.binary_version !== "0.41.3" || invocationPath !== realpathSync(driver) || JSON.stringify(manifest.mcp_invocation?.args) !== JSON.stringify(["mcp"]) ) { fail(`${label} CUA manifest does not match the packaged executable`); } for (const name of LICENSE_FILES) { requireRegularMode(path.join(licenses, name), 0o644); requireContained(licenses, path.join(licenses, name)); } requireRegularMode(path.join(cuaRoot, "release.json"), 0o644); const release = JSON.parse(readFileSync(path.join(cuaRoot, "release.json"), "utf8")); const expectedRelease = { schemaVersion: 1, version: "0.41.3", platform: "linux", arch: "x64", archive: { name: "cua-driver-rs-0.41.3-linux-x86_64-binary.tar.gz", url: "https://github.com/trycua/cua/releases/download/cua-driver-rs-v0.41.3/" + "cua-driver-rs-0.41.3-linux-x86_64-binary.tar.gz", size: 38_375_345, sha256: "a00a7bc4a648adbcbc93bfbf5a058daf71ce8838ee172e33b197aaf649ecee14", }, sourceCommit: "bd0e299c67dec6397b3d41a259452b2ea13a1cd5", files: { "cua-driver": { sha256: expectedHashes.get(driver), mode: "0755" }, "cua-cursor-theme": { sha256: expectedHashes.get(cursorTheme), mode: "0755" }, }, }; if (JSON.stringify(release) !== JSON.stringify(expectedRelease)) { fail(`${label} release.json does not match the reviewed Linux CUA release`); } const sbom = JSON.parse(readFileSync(path.join(licenses, "SBOM.cdx.json"), "utf8")); const components = new Set( (sbom.components ?? []).map((component) => `${component.name}@${component.version}`), ); for (const expected of ["uniffi_core@0.31.0", "option-ext@0.2.0"]) { if (!components.has(expected)) fail(`${label} SBOM is missing ${expected}`); } verifyCompliance(licenses, label); if (validateRuntimePath) { const validation = validateDriverCandidate(driver); if (validation.status !== "found" || validation.path !== realpathSync(driver)) { fail(`${label} packaged CUA path fails the runtime trust policy: ${JSON.stringify(validation)}`); } } return expectedHashes; } function verifyCloudflaredResources(resources, label, { directoryMode = 0o755 } = {}) { const cloudflaredRoot = path.join(resources, "cloudflared"); const executable = path.join(cloudflaredRoot, "cloudflared"); requireDirectoryMode(cloudflaredRoot, directoryMode); requireExactEntries(cloudflaredRoot, ["cloudflared"]); requireContained(resources, cloudflaredRoot); requireRegularMode(executable, 0o755); requireContained(cloudflaredRoot, executable); const expectedHash = CLOUDFLARED_ASSETS["linux-x64"].binarySha256; const actualHash = sha256(executable); if (actualHash !== expectedHash) { fail(`${label} has the wrong hash for cloudflared: ${actualHash}`); } if (executableTarget(readFileSync(executable)) !== "linux-x64") { fail(`${label} cloudflared does not contain the reviewed Linux x64 executable`); } const version = execFileSync(executable, ["version"], { encoding: "utf8", timeout: 5_000, }).trim(); if (!version.startsWith(`cloudflared version ${CLOUDFLARED_VERSION} `)) { fail(`${label} cloudflared version is ${JSON.stringify(version)}`); } const licenses = path.join(resources, "licenses"); requireDirectoryMode(licenses, directoryMode); for (const name of ["cloudflared-LICENSE.txt", "cloudflared-README.md"]) { requireRegularMode(path.join(licenses, name), 0o644); requireContained(licenses, path.join(licenses, name)); } if (sha256(path.join(licenses, "cloudflared-LICENSE.txt")) !== sha256(path.join(root, "LICENSE"))) { fail(`${label} cloudflared license text differs from the reviewed Apache 2.0 text`); } if ( sha256(path.join(licenses, "cloudflared-README.md")) !== sha256(path.join(root, "third_party", "cloudflared", "README.md")) ) { fail(`${label} cloudflared release provenance differs from the reviewed record`); } return actualHash; } const appImage = exactlyOne(".AppImage"); const deb = exactlyOne(".deb"); const unpacked = path.join(releaseDir, "linux-unpacked"); const executable = path.join(unpacked, "openmausbot"); const resources = path.join(unpacked, "resources"); requireExecutable(appImage); requireExecutable(executable); requireDirectoryMode(unpacked, 0o755); for (const relative of ["app.asar", "ui/index.html", "server/index.js", "server/desktop-entry.mjs"]) { requireFile(path.join(resources, relative)); } for (const forbidden of ["speech-helper", "cua-driver", "cua-sdk"]) { if (statSync(path.join(resources, forbidden), { throwIfNoEntry: false })) { fail(`unsupported Linux resource was bundled: ${forbidden}`); } } const unpackedCuaHashes = verifyCuaResources(resources, "linux-unpacked"); const unpackedCloudflaredHash = verifyCloudflaredResources(resources, "linux-unpacked"); requireUpdaterTarget(resources, "linux-unpacked"); const fields = execFileSync( "dpkg-deb", ["--field", deb, "Package", "Version", "Architecture", "Maintainer", "Section", "Priority"], { encoding: "utf8" }, ); for (const expected of [ "Package: openmausbot", "Architecture: amd64", "Maintainer: Milind Soni", "Section: utils", "Priority: optional", ]) { if (!fields.includes(expected)) fail(`DEB metadata is missing ${JSON.stringify(expected)}`); } const extracted = mkdtempSync(path.join(tmpdir(), "omb-deb-verify-")); try { execFileSync("dpkg-deb", ["--extract", deb, extracted]); const debAppRoot = path.join(extracted, "opt", "OpenMausBot"); requireDirectoryMode(debAppRoot, 0o755); const debResources = path.join(debAppRoot, "resources"); // Routes the in-app updater to the package-manager hand-off. requirePackageType(debResources, "DEB", "deb"); requireUpdaterTarget(debResources, "DEB"); const debHashes = verifyCuaResources(debResources, "DEB"); const debCloudflaredHash = verifyCloudflaredResources(debResources, "DEB"); if (debCloudflaredHash !== unpackedCloudflaredHash) { fail(`DEB and linux-unpacked cloudflared hashes differ`); } for (const [unpackedFile, expected] of unpackedCuaHashes) { const packaged = path.join(debResources, "cua-linux-x64", path.basename(unpackedFile)); if (debHashes.get(packaged) !== expected) fail(`DEB and linux-unpacked CUA hashes differ`); } const desktopFile = path.join( extracted, "usr", "share", "applications", "com.openmausbot.app.desktop", ); const scalableIcon = path.join( extracted, "usr", "share", "icons", "hicolor", "scalable", "apps", "openmausbot.svg", ); requireFile(desktopFile); requireFile(scalableIcon); const desktop = readFileSync(desktopFile, "utf8"); for (const expected of [ "Name=OpenMausBot", "Exec=/opt/OpenMausBot/openmausbot %U", "Icon=openmausbot", "StartupWMClass=com.openmausbot.app", "Categories=Utility;", ]) { if (!desktop.includes(expected)) fail(`desktop entry is missing ${JSON.stringify(expected)}`); } execFileSync("desktop-file-validate", [desktopFile], { stdio: "inherit" }); } finally { rmSync(extracted, { recursive: true, force: true }); } const appImageExtracted = mkdtempSync(path.join(tmpdir(), "omb-appimage-verify-")); try { const offset = execFileSync(appImage, ["--appimage-offset"], { encoding: "utf8", timeout: 10_000, }).trim(); if (!/^\d+$/.test(offset)) fail(`AppImage returned an invalid SquashFS offset: ${offset}`); const squashRoot = path.join(appImageExtracted, "squashfs-root"); execFileSync( "unsquashfs", ["-no-progress", "-offset", offset, "-d", squashRoot, appImage], { stdio: ["ignore", "ignore", "inherit"], timeout: 60_000 }, ); const appImageDirectoryMode = lstatSync(squashRoot).mode & 0o777; if (![0o755, 0o775].includes(appImageDirectoryMode)) { fail( `expected AppImage directory mode 755 or 775 for ${squashRoot}, found ${appImageDirectoryMode.toString(8)}`, ); } const appImageResources = path.join(squashRoot, "resources"); // No marker: the AppImage keeps the in-place restart-to-update path. requirePackageType(appImageResources, "AppImage", null); requireUpdaterTarget(appImageResources, "AppImage"); // Depending on the pinned appimagetool runtime, SquashFS directories are // emitted as root:root 0755 or 0775. Require one mode consistently across // the reviewed resource tree. The app never executes through that path: // Electron stages and re-hashes the two pinned binaries into a fresh 0700 // directory first. The AppImage smoke below proves that packaged path, // while this extraction verifies inputs. const appImageHashes = verifyCuaResources(appImageResources, "AppImage", { directoryMode: appImageDirectoryMode, validateRuntimePath: false, }); const appImageCloudflaredHash = verifyCloudflaredResources(appImageResources, "AppImage", { directoryMode: appImageDirectoryMode, }); if (appImageCloudflaredHash !== unpackedCloudflaredHash) { fail(`AppImage and linux-unpacked cloudflared hashes differ`); } for (const [unpackedFile, expected] of unpackedCuaHashes) { const packaged = path.join(appImageResources, "cua-linux-x64", path.basename(unpackedFile)); if (appImageHashes.get(packaged) !== expected) fail(`AppImage and linux-unpacked CUA hashes differ`); } } finally { rmSync(appImageExtracted, { recursive: true, force: true }); } console.log(`[verify-linux-package] OK\n- ${path.basename(appImage)}\n- ${path.basename(deb)}`);