mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-23 22:35:42 +00:00
fix: verify universal macOS binaries reliably
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
collectCodeSigningMetadataErrors,
|
||||
collectEntitlementErrors,
|
||||
expectedMachOArchitecture,
|
||||
hasMachOMagic,
|
||||
parseLipoArchitectures,
|
||||
} from "../scripts/macos-distribution-policy.mjs";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
@@ -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
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
closeSync,
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdtempSync,
|
||||
openSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
readSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
@@ -19,6 +22,7 @@ import {
|
||||
collectArchitectureErrors,
|
||||
collectCodeSigningMetadataErrors,
|
||||
collectEntitlementErrors,
|
||||
hasMachOMagic,
|
||||
} from "./macos-distribution-policy.mjs";
|
||||
|
||||
const projectRoot = process.cwd();
|
||||
@@ -26,7 +30,6 @@ const packageJson = JSON.parse(readFileSync(path.join(projectRoot, "package.json
|
||||
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) {
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user