mirror of
https://github.com/aphrody-labs/mullvadvpn-app.git
synced 2026-10-13 22:10:39 +00:00
Add spawn constraint to macOS launch daemon
On some machines, the app bundle can be replaced by any admin user, so mullvad-daemon can be replaced with a malicious binary that the launch daemon runs as root. This adds a spawn constraint that refuses to run any binary that does not match the expected cdhash of mullvad-daemon. The hash is disabled for unsigned builds since they don't have this hash embedded.
This commit is contained in:
1 parent
611273f67f
commit
05ec268c54
2 files changed
+79
-1
No files matched your search
@@ -2,7 +2,7 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const builder = require('electron-builder');
|
||||
const { Arch } = require('electron-builder');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { execFileSync, spawnSync } = require('child_process');
|
||||
|
||||
const { author } = require('../package.json');
|
||||
const { signWindows } = require('./sign-windows.cjs');
|
||||
@@ -354,6 +354,8 @@ async function packMac() {
|
||||
// them with @electron/universal. Both sub-packs run beforeBuild/beforePack with a concrete
|
||||
// arch (x64/arm64).
|
||||
const isUniversal = arch === 'universal';
|
||||
// Different archs have different cdhashes.
|
||||
const pkgScriptsDir = buildAssets(`pkg-scripts-${arch ?? 'host'}`);
|
||||
return {
|
||||
...config,
|
||||
mac: {
|
||||
@@ -364,6 +366,10 @@ async function packMac() {
|
||||
},
|
||||
artifactName,
|
||||
},
|
||||
pkg: {
|
||||
...config.pkg,
|
||||
scripts: pkgScriptsDir,
|
||||
},
|
||||
asarUnpack: ['**/*.node'],
|
||||
beforeBuild: async (options) => {
|
||||
switch (options.arch) {
|
||||
@@ -400,6 +406,13 @@ async function packMac() {
|
||||
await removeNseventforwarderNativeModules();
|
||||
}
|
||||
config.beforePack?.(context);
|
||||
|
||||
fs.rmSync(pkgScriptsDir, { recursive: true, force: true });
|
||||
|
||||
if (process.env.CSC_IDENTITY_AUTO_DISCOVERY !== 'true') {
|
||||
// afterSign is never called if signing is disabled.
|
||||
writePkgScripts(pkgScriptsDir, '');
|
||||
}
|
||||
},
|
||||
afterPack: (context) => {
|
||||
config.afterPack?.(context);
|
||||
@@ -412,6 +425,11 @@ async function packMac() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
afterAllArtifactBuild: async (_buildResult) => {
|
||||
const postinstall = fs.readFileSync(path.join(pkgScriptsDir, 'postinstall'), 'utf8');
|
||||
if (postinstall.includes(DAEMON_SPAWN_CONSTRAINT_PLACEHOLDER)) {
|
||||
throw new Error('Daemon spawn constraint was not added to postinstall');
|
||||
}
|
||||
|
||||
// Remove the folder that contains the unpacked app. Electron builder cleans up some of
|
||||
// these directories and it's changed between versions without a mention in the changelog.
|
||||
for (const dir of appOutDirs) {
|
||||
@@ -425,6 +443,18 @@ async function packMac() {
|
||||
afterSign: (context) => {
|
||||
const appOutDir = context.appOutDir;
|
||||
appOutDirs.push(appOutDir);
|
||||
|
||||
// Only merged app is included in pkg.
|
||||
if (isUniversal && context.arch !== Arch.universal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const daemonPath = path.join(
|
||||
appOutDir,
|
||||
`${context.packager.appInfo.productFilename}.app`,
|
||||
'Contents/Resources/mullvad-daemon',
|
||||
);
|
||||
writePkgScripts(pkgScriptsDir, daemonSpawnConstraint(daemonPath));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -448,6 +478,49 @@ async function packMac() {
|
||||
});
|
||||
}
|
||||
|
||||
const DAEMON_SPAWN_CONSTRAINT_PLACEHOLDER = '@DAEMON_SPAWN_CONSTRAINT@';
|
||||
|
||||
// Copy the pkg scripts to `outDir`, replacing the spawn constraint placeholder in postinstall with
|
||||
// `spawnConstraint`. If `spawnConstraint` is null, the placeholder is kept.
|
||||
function writePkgScripts(outDir, spawnConstraint) {
|
||||
fs.rmSync(outDir, { recursive: true, force: true });
|
||||
fs.cpSync(distAssets('pkg-scripts'), outDir, { recursive: true });
|
||||
|
||||
const postinstallPath = path.join(outDir, 'postinstall');
|
||||
const postinstall = fs.readFileSync(postinstallPath, 'utf8');
|
||||
fs.writeFileSync(
|
||||
postinstallPath,
|
||||
postinstall.replace(DAEMON_SPAWN_CONSTRAINT_PLACEHOLDER, () => spawnConstraint),
|
||||
);
|
||||
}
|
||||
|
||||
// Return a launchd spawn constraint that only allows the daemon at `daemonPath` to be launched,
|
||||
// identified by its cdhash. A universal binary has one cdhash per architecture.
|
||||
function daemonSpawnConstraint(daemonPath) {
|
||||
const archs = execFileSync('lipo', ['-archs', daemonPath], { encoding: 'utf8' })
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
|
||||
const cdhashes = archs.map((arch) => {
|
||||
// codesign prints the signature info to stderr
|
||||
const { stderr } = spawnSync('codesign', ['--display', '-vvv', '--arch', arch, daemonPath], {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const match = stderr.match(/^CDHash=([0-9a-f]+)$/m);
|
||||
if (!match) {
|
||||
throw new Error(`Failed to get cdhash of ${daemonPath} (${arch})`);
|
||||
}
|
||||
return Buffer.from(match[1], 'hex').toString('base64');
|
||||
});
|
||||
|
||||
const value =
|
||||
cdhashes.length === 1
|
||||
? `<data>${cdhashes[0]}</data>`
|
||||
: `<dict><key>\\$in</key><array>${cdhashes.map((cdhash) => `<data>${cdhash}</data>`).join('')}</array></dict>`;
|
||||
|
||||
return `<key>SpawnConstraint</key><dict><key>cdhash</key>${value}</dict>`;
|
||||
}
|
||||
|
||||
function packLinux() {
|
||||
writeRpmChangelog();
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ fi
|
||||
|
||||
# NOTE: This path must be kept in sync with the path defined
|
||||
# in mullvad-daemon/src/macos_launch_daemon.rs and preinstall
|
||||
#
|
||||
# NOTE: The DAEMON_SPAWN_CONSTRAINT placeholder below is replaced with pinned cdhashes.
|
||||
# See desktop/packages/mullvad-vpn/tasks/distribution.cjs.
|
||||
DAEMON_PLIST_PATH="/Library/LaunchDaemons/net.mullvad.daemon.plist"
|
||||
|
||||
DAEMON_PLIST=$(cat <<-EOM
|
||||
@@ -56,6 +59,8 @@ DAEMON_PLIST=$(cat <<-EOM
|
||||
<key>UserName</key>
|
||||
<string>root</string>
|
||||
|
||||
@DAEMON_SPAWN_CONSTRAINT@
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
|
||||
Reference in new issue
Block a user