From ed8f9653dadc3029f14bf71917bee5d335c20732 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Mon, 2 Feb 2026 06:53:33 -0800 Subject: [PATCH] Sign macOS server binaries (#291872) --- build/azure-pipelines/darwin/codesign.ts | 57 +++++++++---- .../darwin/product-build-darwin.yml | 4 +- .../darwin/server-entitlements.plist | 8 ++ .../steps/product-build-darwin-compile.yml | 32 ++++++-- build/darwin/sign-server.ts | 82 +++++++++++++++++++ 5 files changed, 158 insertions(+), 25 deletions(-) create mode 100644 build/azure-pipelines/darwin/server-entitlements.plist create mode 100644 build/darwin/sign-server.ts diff --git a/build/azure-pipelines/darwin/codesign.ts b/build/azure-pipelines/darwin/codesign.ts index 263f96cd2ff..2cc826d60c8 100644 --- a/build/azure-pipelines/darwin/codesign.ts +++ b/build/azure-pipelines/darwin/codesign.ts @@ -10,27 +10,54 @@ async function main() { const arch = e('VSCODE_ARCH'); const esrpCliDLLPath = e('EsrpCliDllPath'); const pipelineWorkspace = e('PIPELINE_WORKSPACE'); + const buildSourcesDirectory = e('BUILD_SOURCESDIRECTORY'); - const folder = `${pipelineWorkspace}/vscode_client_darwin_${arch}_archive`; + const clientFolder = `${pipelineWorkspace}/vscode_client_darwin_${arch}_archive`; const dmgFolder = `${pipelineWorkspace}/vscode_client_darwin_${arch}_dmg`; - const glob = `VSCode-darwin-${arch}.zip`; + const clientGlob = `VSCode-darwin-${arch}.zip`; const dmgGlob = `VSCode-darwin-${arch}.dmg`; - // Codesign - const archiveCodeSignTask = spawnCodesignProcess(esrpCliDLLPath, 'sign-darwin', folder, glob); - const dmgCodeSignTask = spawnCodesignProcess(esrpCliDLLPath, 'sign-darwin', dmgFolder, dmgGlob); - printBanner('Codesign Archive'); - await streamProcessOutputAndCheckResult('Codesign Archive', archiveCodeSignTask); - printBanner('Codesign DMG'); - await streamProcessOutputAndCheckResult('Codesign DMG', dmgCodeSignTask); + const serverFolder = `${buildSourcesDirectory}/.build/darwin/server`; + const serverGlob = `vscode-server-darwin-${arch}.zip`; + const webGlob = `vscode-server-darwin-${arch}-web.zip`; + + // Start codesign processes in parallel + const codeSignClientTask = spawnCodesignProcess(esrpCliDLLPath, 'sign-darwin', clientFolder, clientGlob); + const codeSignDmgTask = spawnCodesignProcess(esrpCliDLLPath, 'sign-darwin', dmgFolder, dmgGlob); + const codeSignServerTask = spawnCodesignProcess(esrpCliDLLPath, 'sign-darwin', serverFolder, serverGlob); + const codeSignWebTask = spawnCodesignProcess(esrpCliDLLPath, 'sign-darwin', serverFolder, webGlob); + + // Await codesign results + printBanner('Codesign client'); + await streamProcessOutputAndCheckResult('Codesign client', codeSignClientTask); + + printBanner('Codesign DMG'); + await streamProcessOutputAndCheckResult('Codesign DMG', codeSignDmgTask); + + printBanner('Codesign server'); + await streamProcessOutputAndCheckResult('Codesign server', codeSignServerTask); + + printBanner('Codesign web'); + await streamProcessOutputAndCheckResult('Codesign web', codeSignWebTask); + + // Start notarize processes in parallel (after codesigning is complete) + const notarizeClientTask = spawnCodesignProcess(esrpCliDLLPath, 'notarize-darwin', clientFolder, clientGlob); + const notarizeDmgTask = spawnCodesignProcess(esrpCliDLLPath, 'notarize-darwin', dmgFolder, dmgGlob); + const notarizeServerTask = spawnCodesignProcess(esrpCliDLLPath, 'notarize-darwin', serverFolder, serverGlob); + const notarizeWebTask = spawnCodesignProcess(esrpCliDLLPath, 'notarize-darwin', serverFolder, webGlob); + + // Await notarize results + printBanner('Notarize client'); + await streamProcessOutputAndCheckResult('Notarize client', notarizeClientTask); - // Notarize - const archiveNotarizeTask = spawnCodesignProcess(esrpCliDLLPath, 'notarize-darwin', folder, glob); - const dmgNotarizeTask = spawnCodesignProcess(esrpCliDLLPath, 'notarize-darwin', dmgFolder, dmgGlob); - printBanner('Notarize Archive'); - await streamProcessOutputAndCheckResult('Notarize Archive', archiveNotarizeTask); printBanner('Notarize DMG'); - await streamProcessOutputAndCheckResult('Notarize DMG', dmgNotarizeTask); + await streamProcessOutputAndCheckResult('Notarize DMG', notarizeDmgTask); + + printBanner('Notarize server'); + await streamProcessOutputAndCheckResult('Notarize server', notarizeServerTask); + + printBanner('Notarize web'); + await streamProcessOutputAndCheckResult('Notarize web', notarizeWebTask); } main().then(() => { diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 37282e92146..339fe6209ae 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -57,14 +57,14 @@ jobs: sbomPackageVersion: $(Build.SourceVersion) - output: pipelineArtifact targetPath: $(Build.ArtifactStagingDirectory)/out/vscode_server_darwin_$(VSCODE_ARCH)_archive/vscode-server-darwin-$(VSCODE_ARCH).zip - artifactName: vscode_server_darwin_$(VSCODE_ARCH)_archive-unsigned + artifactName: vscode_server_darwin_$(VSCODE_ARCH)_archive displayName: Publish server archive sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH) sbomPackageName: "VS Code macOS $(VSCODE_ARCH) Server" sbomPackageVersion: $(Build.SourceVersion) - output: pipelineArtifact targetPath: $(Build.ArtifactStagingDirectory)/out/vscode_web_darwin_$(VSCODE_ARCH)_archive/vscode-server-darwin-$(VSCODE_ARCH)-web.zip - artifactName: vscode_web_darwin_$(VSCODE_ARCH)_archive-unsigned + artifactName: vscode_web_darwin_$(VSCODE_ARCH)_archive displayName: Publish web server archive sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH)-web sbomPackageName: "VS Code macOS $(VSCODE_ARCH) Web" diff --git a/build/azure-pipelines/darwin/server-entitlements.plist b/build/azure-pipelines/darwin/server-entitlements.plist new file mode 100644 index 00000000000..4efe1ce508f --- /dev/null +++ b/build/azure-pipelines/darwin/server-entitlements.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.allow-jit + + + diff --git a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml index e20ef8deb01..7604d54909f 100644 --- a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml +++ b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml @@ -134,10 +134,6 @@ steps: set -e npm run gulp vscode-reh-darwin-$(VSCODE_ARCH)-min-ci mv ../vscode-reh-darwin-$(VSCODE_ARCH) ../vscode-server-darwin-$(VSCODE_ARCH) # TODO@joaomoreno - ARCHIVE_PATH=".build/darwin/server/vscode-server-darwin-$(VSCODE_ARCH).zip" - mkdir -p $(dirname $ARCHIVE_PATH) - (cd .. && zip -Xry $(Build.SourcesDirectory)/$ARCHIVE_PATH vscode-server-darwin-$(VSCODE_ARCH)) - echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server @@ -146,10 +142,6 @@ steps: set -e npm run gulp vscode-reh-web-darwin-$(VSCODE_ARCH)-min-ci mv ../vscode-reh-web-darwin-$(VSCODE_ARCH) ../vscode-server-darwin-$(VSCODE_ARCH)-web # TODO@joaomoreno - ARCHIVE_PATH=".build/darwin/server/vscode-server-darwin-$(VSCODE_ARCH)-web.zip" - mkdir -p $(dirname $ARCHIVE_PATH) - (cd .. && zip -Xry $(Build.SourcesDirectory)/$ARCHIVE_PATH vscode-server-darwin-$(VSCODE_ARCH)-web) - echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server (web) @@ -224,6 +216,13 @@ steps: DEBUG=electron-osx-sign* node build/darwin/sign.ts $(agent.builddirectory) displayName: Set Hardened Entitlements + - script: | + set -e + export CODESIGN_IDENTITY=$(security find-identity -v -p codesigning $(agent.tempdirectory)/buildagent.keychain | grep -oEi "([0-9A-F]{40})" | head -n 1) + node build/darwin/sign-server.ts $(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH) + node build/darwin/sign-server.ts $(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH)-web + displayName: Sign server binaries + - script: | set -e # Needed for https://github.com/dmgbuild/dmgbuild/blob/main/src/dmgbuild/badge.py @@ -245,6 +244,22 @@ steps: condition: eq(variables['BUILT_CLIENT'], 'true') displayName: Re-package client after entitlement + - script: | + set -e + ARCHIVE_PATH=".build/darwin/server/vscode-server-darwin-$(VSCODE_ARCH).zip" + mkdir -p $(dirname $ARCHIVE_PATH) + (cd .. && zip -Xry $(Build.SourcesDirectory)/$ARCHIVE_PATH vscode-server-darwin-$(VSCODE_ARCH)) + echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" + displayName: Package server + + - script: | + set -e + ARCHIVE_PATH=".build/darwin/server/vscode-server-darwin-$(VSCODE_ARCH)-web.zip" + mkdir -p $(dirname $ARCHIVE_PATH) + (cd .. && zip -Xry $(Build.SourcesDirectory)/$ARCHIVE_PATH vscode-server-darwin-$(VSCODE_ARCH)-web) + echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" + displayName: Package server (web) + - task: UseDotNet@2 inputs: version: 6.x @@ -273,6 +288,7 @@ steps: env: EsrpCliDllPath: $(EsrpCliDllPath) SYSTEM_ACCESSTOKEN: $(System.AccessToken) + BUILD_SOURCESDIRECTORY: $(Build.SourcesDirectory) displayName: ✍️ Codesign & Notarize - ${{ if or(eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true), eq(parameters.VSCODE_RUN_BROWSER_TESTS, true), eq(parameters.VSCODE_RUN_REMOTE_TESTS, true)) }}: diff --git a/build/darwin/sign-server.ts b/build/darwin/sign-server.ts new file mode 100644 index 00000000000..308258b5e52 --- /dev/null +++ b/build/darwin/sign-server.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { spawn } from '@malept/cross-spawn-promise'; +import fs from 'fs'; +import path from 'path'; + +const MACHO_MAGIC_NUMBERS = new Set([ + 0xFEEDFACE, // MH_MAGIC (32-bit) + 0xCEFAEDFE, // MH_CIGAM (32-bit, byte-swapped) + 0xFEEDFACF, // MH_MAGIC_64 (64-bit) + 0xCFFAEDFE, // MH_CIGAM_64 (64-bit, byte-swapped) + 0xCAFEBABE, // FAT_MAGIC (universal binary) + 0xBEBAFECA, // FAT_CIGAM (universal binary, byte-swapped) +]); + +function isMachOBinary(filePath: string): boolean { + try { + let fd: number | undefined; + try { + fd = fs.openSync(filePath, 'r'); + const buffer = Buffer.alloc(4); + fs.readSync(fd, buffer, 0, 4, 0); + const magic = buffer.readUInt32BE(0); + return MACHO_MAGIC_NUMBERS.has(magic); + } finally { + if (fd !== undefined) { + fs.closeSync(fd); + } + } + } catch { + return false; + } +} + +async function main(serverDir: string): Promise { + if (!serverDir || !fs.existsSync(serverDir)) { + throw new Error('Server directory argument is required'); + } + + const tempDir = process.env['AGENT_TEMPDIRECTORY']; + if (!tempDir) { + throw new Error('$AGENT_TEMPDIRECTORY not set'); + } + + const identity = process.env['CODESIGN_IDENTITY']; + if (!identity) { + throw new Error('$CODESIGN_IDENTITY not set'); + } + + const keychain = path.join(tempDir, 'buildagent.keychain'); + const baseDir = path.dirname(import.meta.dirname); + const entitlementsPath = path.join(baseDir, 'azure-pipelines', 'darwin', 'server-entitlements.plist'); + + console.log(`Signing Mach-O binaries in: ${serverDir}`); + for (const entry of fs.readdirSync(serverDir, { withFileTypes: true, recursive: true })) { + if (entry.isFile()) { + const filePath = path.join(entry.parentPath, entry.name); + if (isMachOBinary(filePath)) { + console.log(`Signing: ${filePath}`); + await spawn('codesign', [ + '--sign', identity, + '--keychain', keychain, + '--options', 'runtime', + '--timestamp', + '--force', + '--entitlements', entitlementsPath, + filePath + ]); + } + } + } +} + +if (import.meta.main) { + main(process.argv[2]).catch(err => { + console.error(err); + process.exit(1); + }); +}