Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions electron/macosDistributionPolicy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
collectCodeSigningMetadataErrors,
collectEntitlementErrors,
expectedMachOArchitecture,
hasMachOMagic,
parseLipoArchitectures,
} from "../scripts/macos-distribution-policy.mjs";

Expand Down Expand Up @@ -95,6 +96,20 @@ describe("macOS distribution entitlement policy", () => {
});

describe("macOS distribution architecture policy", () => {
it("recognizes thin and universal Mach-O magic bytes without parsing file output", () => {
for (const header of [
[0xfe, 0xed, 0xfa, 0xce],
[0xcf, 0xfa, 0xed, 0xfe],
[0xca, 0xfe, 0xba, 0xbe],
[0xbf, 0xba, 0xfe, 0xca],
]) {
expect(hasMachOMagic(Uint8Array.from(header))).toBe(true);
}

expect(hasMachOMagic(Uint8Array.from([0x7f, 0x45, 0x4c, 0x46]))).toBe(false);
expect(hasMachOMagic(Uint8Array.from([0xfe, 0xed, 0xfa]))).toBe(false);
});

it("parses thin and fat lipo output", () => {
expect(parseLipoArchitectures("Non-fat file: App is architecture: arm64")).toEqual([
"arm64",
Expand Down
13 changes: 13 additions & 0 deletions scripts/macos-distribution-policy.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
const TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/;

const MACH_O_MAGIC_NUMBERS = new Set([
0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe, 0xcafebabf, 0xbebafeca, 0xbfbafeca,
]);

export const REQUIRED_MACOS_ENTITLEMENTS = Object.freeze([
"com.apple.security.cs.allow-jit",
"com.apple.security.cs.allow-unsigned-executable-memory",
Expand All @@ -25,6 +29,15 @@ export function assertValidAppleTeamId(teamId) {
}
}

export function hasMachOMagic(header) {
if (!(header instanceof Uint8Array) || header.byteLength < 4) {
return false;
}

const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
return MACH_O_MAGIC_NUMBERS.has(view.getUint32(0, false));
}

export function collectCodeSigningMetadataErrors(details, expectedTeamId) {
const errors = [];
const authorities = details
Expand Down
34 changes: 16 additions & 18 deletions scripts/verify-macos-distribution.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import { spawnSync } from "node:child_process";
import {
closeSync,
existsSync,
lstatSync,
mkdtempSync,
openSync,
readdirSync,
readFileSync,
readSync,
rmSync,
statSync,
writeFileSync,
Expand All @@ -19,14 +22,14 @@ import {
collectArchitectureErrors,
collectCodeSigningMetadataErrors,
collectEntitlementErrors,
hasMachOMagic,
} from "./macos-distribution-policy.mjs";

const projectRoot = process.cwd();
const packageJson = JSON.parse(readFileSync(path.join(projectRoot, "package.json"), "utf8"));
const productName = packageJson.productName ?? packageJson.name ?? "Recordly";
const expectedBundleId = "dev.recordly.app";
const commandTimeoutMs = 5 * 60 * 1000;
const fileClassificationBatchSize = 100;
const maxReportDetailLength = 4_000;

function parseArguments(argv) {
Expand Down Expand Up @@ -174,6 +177,17 @@ function walkRegularFiles(rootPath) {
return files;
}

function isMachOBinary(filePath) {
const header = new Uint8Array(4);
const descriptor = openSync(filePath, "r");
try {
const bytesRead = readSync(descriptor, header, 0, header.byteLength, 0);
return bytesRead === header.byteLength && hasMachOMagic(header);
} finally {
closeSync(descriptor);
}
}

function extractPlist(commandResult) {
const output = [commandResult.stdout, commandResult.stderr].filter(Boolean).join("\n");
const xmlStart = output.indexOf("<?xml");
Expand Down Expand Up @@ -281,23 +295,7 @@ function verifyEntitlements(appPath, label, tempRoot, check) {

function verifyMachOBinaries(appPath, arch, check) {
check("packaged app: nested Mach-O signatures and architectures", () => {
const machOBinaries = [];
const regularFiles = walkRegularFiles(appPath);
for (let index = 0; index < regularFiles.length; index += fileClassificationBatchSize) {
const batch = regularFiles.slice(index, index + fileClassificationBatchSize);
const fileTypes = runProcess("file", ["-b", ...batch]).stdout.split(/\r?\n/);
if (fileTypes.length !== batch.length) {
throw new Error(
`file classification returned ${fileTypes.length} rows for ${batch.length} paths`,
);
}

for (let batchIndex = 0; batchIndex < batch.length; batchIndex += 1) {
if (fileTypes[batchIndex].includes("Mach-O")) {
machOBinaries.push(batch[batchIndex]);
}
}
}
const machOBinaries = walkRegularFiles(appPath).filter(isMachOBinary);

if (machOBinaries.length === 0) {
throw new Error("no Mach-O binaries were found in the app bundle");
Expand Down