Add Codesign validation for MacOS

This commit is contained in:
Dmitriy Vasyura
2026-01-11 14:21:32 +01:00
parent 99c7bf3cb2
commit a03c09c05e
5 changed files with 97 additions and 22 deletions
+4 -2
View File
@@ -28,6 +28,7 @@ export function setup(context: TestContext) {
if (context.platform === 'darwin-arm64') {
it('cli-darwin-arm64', async () => {
const dir = await context.downloadAndUnpack('cli-darwin-arm64');
context.validateAllCodesignSignatures(dir);
const entryPoint = context.getEntryPoint('cli', dir);
await testCliApp(entryPoint);
});
@@ -36,6 +37,7 @@ export function setup(context: TestContext) {
if (context.platform === 'darwin-x64') {
it('cli-darwin-x64', async () => {
const dir = await context.downloadAndUnpack('cli-darwin-x64');
context.validateAllCodesignSignatures(dir);
const entryPoint = context.getEntryPoint('cli', dir);
await testCliApp(entryPoint);
});
@@ -68,7 +70,7 @@ export function setup(context: TestContext) {
if (context.platform === 'win32-arm64') {
it('cli-win32-arm64', async () => {
const dir = await context.downloadAndUnpack('cli-win32-arm64');
context.validateAllSignatures(dir);
context.validateAllAuthenticodeSignatures(dir);
const entryPoint = context.getEntryPoint('cli', dir);
await testCliApp(entryPoint);
});
@@ -77,7 +79,7 @@ export function setup(context: TestContext) {
if (context.platform === 'win32-x64') {
it('cli-win32-x64', async () => {
const dir = await context.downloadAndUnpack('cli-win32-x64');
context.validateAllSignatures(dir);
context.validateAllAuthenticodeSignatures(dir);
const entryPoint = context.getEntryPoint('cli', dir);
await testCliApp(entryPoint);
});
+72 -6
View File
@@ -192,7 +192,7 @@ export class TestContext {
this.validateSha256Hash(filePath, sha256hash);
if (TestContext.authenticodeInclude.test(filePath) && os.platform() === 'win32') {
this.validateSignature(filePath);
this.validateAuthenticodeSignature(filePath);
}
return filePath;
@@ -218,7 +218,7 @@ export class TestContext {
* Validates the Authenticode signature of a Windows executable.
* @param filePath The path to the file to validate.
*/
public validateSignature(filePath: string) {
public validateAuthenticodeSignature(filePath: string) {
this.log(`Validating Authenticode signature for ${filePath}`);
const result = this.run('powershell', '-Command', `Get-AuthenticodeSignature "${filePath}" | Select-Object -ExpandProperty Status`);
@@ -233,21 +233,87 @@ export class TestContext {
}
/**
* Validates signatures for all executable files in the specified directory.
* Validates Authenticode signatures for all executable files in the specified directory.
* @param dir The directory to scan for executable files.
*/
public validateAllSignatures(dir: string) {
public validateAllAuthenticodeSignatures(dir: string) {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
const filePath = path.join(dir, file.name);
if (file.isDirectory()) {
this.validateAllSignatures(filePath);
this.validateAllAuthenticodeSignatures(filePath);
} else if (TestContext.authenticodeInclude.test(file.name)) {
this.validateSignature(filePath);
this.validateAuthenticodeSignature(filePath);
}
}
}
/**
* Validates the codesign signature of a macOS binary or app bundle.
* @param filePath The path to the file or app bundle to validate.
*/
public validateCodesignSignature(filePath: string) {
this.log(`Validating codesign signature for ${filePath}`);
const result = this.run('codesign', '--verify', '--deep', '--strict', filePath);
if (result.error !== undefined) {
this.error(`Failed to run codesign: ${result.error.message}`);
}
if (result.status !== 0) {
this.error(`Codesign signature is not valid for ${filePath}: ${result.stderr}`);
}
}
/**
* Validates codesign signatures for all Mach-O binaries in the specified directory.
* @param dir The directory to scan for Mach-O binaries.
*/
public validateAllCodesignSignatures(dir: string) {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
const filePath = path.join(dir, file.name);
if (file.isDirectory()) {
// For .app bundles, validate the bundle itself, not its contents
if (file.name.endsWith('.app') || file.name.endsWith('.framework')) {
this.validateCodesignSignature(filePath);
} else {
this.validateAllCodesignSignatures(filePath);
}
} else if (this.isMachOBinary(filePath)) {
this.validateCodesignSignature(filePath);
}
}
}
/**
* Checks if a file is a Mach-O binary by examining its magic number.
* @param filePath The path to the file to check.
* @returns True if the file is a Mach-O binary.
*/
private isMachOBinary(filePath: string): boolean {
try {
const fd = fs.openSync(filePath, 'r');
const buffer = Buffer.alloc(4);
fs.readSync(fd, buffer, 0, 4, 0);
fs.closeSync(fd);
// Mach-O magic numbers:
// MH_MAGIC: 0xFEEDFACE (32-bit)
// MH_CIGAM: 0xCEFAEDFE (32-bit, byte-swapped)
// MH_MAGIC_64: 0xFEEDFACF (64-bit)
// MH_CIGAM_64: 0xCFFAEDFE (64-bit, byte-swapped)
// FAT_MAGIC: 0xCAFEBABE (universal binary)
// FAT_CIGAM: 0xBEBAFECA (universal binary, byte-swapped)
const magic = buffer.readUInt32BE(0);
return magic === 0xFEEDFACE || magic === 0xCEFAEDFE ||
magic === 0xFEEDFACF || magic === 0xCFFAEDFE ||
magic === 0xCAFEBABE || magic === 0xBEBAFECA;
} catch {
return false;
}
}
/**
* Downloads and unpacks the specified VS Code release target.
* @param target The target platform (e.g., 'cli-linux-x64').
+13 -10
View File
@@ -13,6 +13,7 @@ export function setup(context: TestContext) {
if (context.platform === 'darwin-x64') {
it('desktop-darwin-x64', async () => {
const dir = await context.downloadAndUnpack('darwin');
context.validateAllCodesignSignatures(dir);
const entryPoint = context.installMacApp(dir);
await testDesktopApp(entryPoint);
});
@@ -21,6 +22,7 @@ export function setup(context: TestContext) {
if (context.platform === 'darwin-arm64') {
it('desktop-darwin-arm64', async () => {
const dir = await context.downloadAndUnpack('darwin-arm64');
context.validateAllCodesignSignatures(dir);
const entryPoint = context.installMacApp(dir);
await testDesktopApp(entryPoint);
});
@@ -29,6 +31,7 @@ export function setup(context: TestContext) {
if (context.platform === 'darwin-arm64' || context.platform === 'darwin-x64') {
it('desktop-darwin-universal', async () => {
const dir = await context.downloadAndUnpack('darwin-universal');
context.validateAllCodesignSignatures(dir);
const entryPoint = context.installMacApp(dir);
await testDesktopApp(entryPoint, { universal: true });
});
@@ -117,9 +120,9 @@ export function setup(context: TestContext) {
if (context.platform === 'win32-arm64') {
it('desktop-win32-arm64', async () => {
const packagePath = await context.downloadTarget('win32-arm64');
context.validateSignature(packagePath);
context.validateAuthenticodeSignature(packagePath);
const entryPoint = context.installWindowsApp('system', packagePath);
context.validateAllSignatures(path.dirname(entryPoint));
context.validateAllAuthenticodeSignatures(path.dirname(entryPoint));
await testDesktopApp(entryPoint);
await context.uninstallWindowsApp('system');
});
@@ -128,7 +131,7 @@ export function setup(context: TestContext) {
if (context.platform === 'win32-arm64') {
it('desktop-win32-arm64-archive', async () => {
const dir = await context.downloadAndUnpack('win32-arm64-archive');
context.validateAllSignatures(dir);
context.validateAllAuthenticodeSignatures(dir);
const entryPoint = context.getEntryPoint('desktop', dir);
await testDesktopApp(entryPoint);
});
@@ -137,9 +140,9 @@ export function setup(context: TestContext) {
if (context.platform === 'win32-arm64') {
it('desktop-win32-arm64-user', async () => {
const packagePath = await context.downloadTarget('win32-arm64-user');
context.validateSignature(packagePath);
context.validateAuthenticodeSignature(packagePath);
const entryPoint = context.installWindowsApp('user', packagePath);
context.validateAllSignatures(path.dirname(entryPoint));
context.validateAllAuthenticodeSignatures(path.dirname(entryPoint));
await testDesktopApp(entryPoint);
await context.uninstallWindowsApp('user');
});
@@ -148,9 +151,9 @@ export function setup(context: TestContext) {
if (context.platform === 'win32-x64') {
it('desktop-win32-x64', async () => {
const packagePath = await context.downloadTarget('win32-x64');
context.validateSignature(packagePath);
context.validateAuthenticodeSignature(packagePath);
const entryPoint = context.installWindowsApp('system', packagePath);
context.validateAllSignatures(path.dirname(entryPoint));
context.validateAllAuthenticodeSignatures(path.dirname(entryPoint));
await testDesktopApp(entryPoint);
await context.uninstallWindowsApp('system');
});
@@ -159,7 +162,7 @@ export function setup(context: TestContext) {
if (context.platform === 'win32-x64') {
it('desktop-win32-x64-archive', async () => {
const dir = await context.downloadAndUnpack('win32-x64-archive');
context.validateAllSignatures(dir);
context.validateAllAuthenticodeSignatures(dir);
const entryPoint = context.getEntryPoint('desktop', dir);
await testDesktopApp(entryPoint);
});
@@ -168,9 +171,9 @@ export function setup(context: TestContext) {
if (context.platform === 'win32-x64') {
it('desktop-win32-x64-user', async () => {
const packagePath = await context.downloadTarget('win32-x64-user');
context.validateSignature(packagePath);
context.validateAuthenticodeSignature(packagePath);
const entryPoint = context.installWindowsApp('user', packagePath);
context.validateAllSignatures(path.dirname(entryPoint));
context.validateAllAuthenticodeSignatures(path.dirname(entryPoint));
await testDesktopApp(entryPoint);
await context.uninstallWindowsApp('user');
});
+4 -2
View File
@@ -29,6 +29,7 @@ export function setup(context: TestContext) {
if (context.platform === 'darwin-arm64') {
it('server-darwin-arm64', async () => {
const dir = await context.downloadAndUnpack('server-darwin-arm64');
context.validateAllCodesignSignatures(dir);
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
@@ -37,6 +38,7 @@ export function setup(context: TestContext) {
if (context.platform === 'darwin-x64') {
it('server-darwin-x64', async () => {
const dir = await context.downloadAndUnpack('server-darwin');
context.validateAllCodesignSignatures(dir);
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
@@ -69,7 +71,7 @@ export function setup(context: TestContext) {
if (context.platform === 'win32-arm64') {
it('server-win32-arm64', async () => {
const dir = await context.downloadAndUnpack('server-win32-arm64');
context.validateAllSignatures(dir);
context.validateAllAuthenticodeSignatures(dir);
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
@@ -78,7 +80,7 @@ export function setup(context: TestContext) {
if (context.platform === 'win32-x64') {
it('server-win32-x64', async () => {
const dir = await context.downloadAndUnpack('server-win32-x64');
context.validateAllSignatures(dir);
context.validateAllAuthenticodeSignatures(dir);
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
+4 -2
View File
@@ -29,6 +29,7 @@ export function setup(context: TestContext) {
if (context.platform === 'darwin-arm64') {
it('server-web-darwin-arm64', async () => {
const dir = await context.downloadAndUnpack('server-darwin-arm64-web');
context.validateAllCodesignSignatures(dir);
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
@@ -37,6 +38,7 @@ export function setup(context: TestContext) {
if (context.platform === 'darwin-x64') {
it('server-web-darwin-x64', async () => {
const dir = await context.downloadAndUnpack('server-darwin-web');
context.validateAllCodesignSignatures(dir);
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
@@ -69,7 +71,7 @@ export function setup(context: TestContext) {
if (context.platform === 'win32-arm64') {
it('server-web-win32-arm64', async () => {
const dir = await context.downloadAndUnpack('server-win32-arm64-web');
context.validateAllSignatures(dir);
context.validateAllAuthenticodeSignatures(dir);
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});
@@ -78,7 +80,7 @@ export function setup(context: TestContext) {
if (context.platform === 'win32-x64') {
it('server-web-win32-x64', async () => {
const dir = await context.downloadAndUnpack('server-win32-x64-web');
context.validateAllSignatures(dir);
context.validateAllAuthenticodeSignatures(dir);
const entryPoint = context.getServerEntryPoint(dir);
await testServer(entryPoint);
});