From 7fcb1d8b4b5e9742f0e7228d62c4818ba760817e Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Fri, 29 May 2026 15:14:00 +1000 Subject: [PATCH 1/2] Remove Windows PFX signing fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Azure Trusted Signing has been validated in shipped builds (v1.8.2, v1.9.0) and was already the only path CI ever took — `USE_AZURE_TRUSTED_SIGNING` was hardcoded to `1` in both pipelines, so the PFX branches were dead fallback. `windowsSign.ts` now gates on `CI && win32` rather than the env var, since the config is loaded by Forge on every platform and an unconditional Azure config would throw on Mac/Linux CI where the Azure env vars are absent. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- .buildkite/commands/build-for-windows.ps1 | 15 +-- .buildkite/pipeline.yml | 2 - .buildkite/release-build-and-distribute.yml | 2 - apps/studio/forge.config.ts | 12 +-- apps/studio/windowsSign.ts | 18 ++-- scripts/package-appx.mjs | 108 ++++++++------------ 6 files changed, 56 insertions(+), 101 deletions(-) diff --git a/.buildkite/commands/build-for-windows.ps1 b/.buildkite/commands/build-for-windows.ps1 index 0d8847cf14..8abfcf2704 100644 --- a/.buildkite/commands/build-for-windows.ps1 +++ b/.buildkite/commands/build-for-windows.ps1 @@ -28,17 +28,10 @@ if ($Architecture -notin $VALID_ARCHITECTURES) { Exit 1 } -$useAzureTrustedSigning = "$($env:USE_AZURE_TRUSTED_SIGNING)".Trim().ToLower() -If (@('1', 'true') -contains $useAzureTrustedSigning) { - Write-Host "--- :lock: Setting up Azure Trusted Signing" - $setupScript = (Get-Command setup_azure_trusted_signing.ps1 -ErrorAction Stop).Source - & $setupScript - If ($LastExitCode -ne 0) { Exit $LastExitCode } -} Else { - # setup_windows_code_signing.ps1 comes from CI Toolkit Plugin - & "setup_windows_code_signing.ps1" - If ($LastExitCode -ne 0) { Exit $LastExitCode } -} +Write-Host "--- :lock: Setting up Azure Trusted Signing" +$setupScript = (Get-Command setup_azure_trusted_signing.ps1 -ErrorAction Stop).Source +& $setupScript +If ($LastExitCode -ne 0) { Exit $LastExitCode } Write-Host "--- :npm: Installing Node dependencies" bash .buildkite/commands/install-node-dependencies.sh diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 9bdeafbaee..1771fd4651 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -200,8 +200,6 @@ steps: - label: 🔨 Windows Dev Build - {{matrix}} agents: queue: windows - env: - USE_AZURE_TRUSTED_SIGNING: 1 command: powershell -File .buildkite/commands/build-for-windows.ps1 -BuildType dev -Architecture {{matrix}} artifact_paths: - apps\studio\out\**\studio-setup.exe diff --git a/.buildkite/release-build-and-distribute.yml b/.buildkite/release-build-and-distribute.yml index 742c55b4f3..3e0340dd19 100644 --- a/.buildkite/release-build-and-distribute.yml +++ b/.buildkite/release-build-and-distribute.yml @@ -68,8 +68,6 @@ steps: - label: 🔨 Windows Release Build - {{matrix}} agents: queue: windows - env: - USE_AZURE_TRUSTED_SIGNING: 1 command: | bash .buildkite/commands/checkout-release-branch.sh "${RELEASE_VERSION}" powershell -File .buildkite/commands/build-for-windows.ps1 -BuildType release -Architecture {{matrix}} diff --git a/apps/studio/forge.config.ts b/apps/studio/forge.config.ts index 8cf4352617..6989f0ad3f 100644 --- a/apps/studio/forge.config.ts +++ b/apps/studio/forge.config.ts @@ -116,15 +116,9 @@ const config: ForgeConfig = { setupExe: 'studio-setup.exe', - // Azure mode: use the custom signing hook that calls signtool - // with Azure Trusted Signing parameters. - // PFX mode: use the local certificate file and password. - ...( windowsSign - ? { windowsSign } - : { - certificateFile: path.join( repoRoot, 'certificate.pfx' ), - certificatePassword: process.env.WINDOWS_CODE_SIGNING_CERT_PASSWORD, - } ), + // Sign via the custom Azure Trusted Signing hook (signtool, SHA256-only). + // Undefined off Windows CI, where the build is left unsigned. + ...( windowsSign ? { windowsSign } : {} ), }, [ 'win32' ] ), diff --git a/apps/studio/windowsSign.ts b/apps/studio/windowsSign.ts index c768dfdc5b..fcf6885316 100644 --- a/apps/studio/windowsSign.ts +++ b/apps/studio/windowsSign.ts @@ -7,23 +7,19 @@ import type { WindowsSignOptions } from '@electron/packager'; // dual-signs (SHA1 + SHA256), but Azure only supports SHA256. // The hook calls signtool directly with SHA256-only parameters. // -// Controlled by the USE_AZURE_TRUSTED_SIGNING env var: -// - Unset or not '1'/'true': returns undefined, letting Forge use PFX certificate signing. -// - '1' or 'true': returns the Azure signing hook config, or throws if the -// required Azure env vars are missing. +// Only signs on Windows CI. This config is loaded by Forge on every platform, +// so non-Windows builds (and local Windows builds) get undefined — returning +// the Azure config unconditionally would throw on Mac/Linux CI, where the +// Azure env vars are absent. function getWindowsSign(): WindowsSignOptions | undefined { - const useAzureSigning = [ '1', 'true' ].includes( - ( process.env.USE_AZURE_TRUSTED_SIGNING ?? '' ).trim().toLowerCase() - ); - - if ( ! useAzureSigning ) { + if ( ! process.env.CI || process.platform !== 'win32' ) { return undefined; } if ( ! process.env.AZURE_CODE_SIGNING_DLIB || ! process.env.AZURE_METADATA_JSON || ! process.env.SIGNTOOL_PATH ) { throw new Error( - 'USE_AZURE_TRUSTED_SIGNING is set but Azure signing env vars ' + - '(AZURE_CODE_SIGNING_DLIB, AZURE_METADATA_JSON, SIGNTOOL_PATH) are missing. ' + + 'Windows CI build is missing Azure Trusted Signing env vars ' + + '(AZURE_CODE_SIGNING_DLIB, AZURE_METADATA_JSON, SIGNTOOL_PATH). ' + 'Did setup_azure_trusted_signing.ps1 run?' ); } diff --git a/scripts/package-appx.mjs b/scripts/package-appx.mjs index d413e4e347..85bf143461 100644 --- a/scripts/package-appx.mjs +++ b/scripts/package-appx.mjs @@ -4,28 +4,18 @@ import path from 'path'; import convertToWindowsStore from 'electron2appx'; import packageJson from '../apps/studio/package.json' with { type: 'json' }; -const useAzureSigning = [ '1', 'true' ].includes( - ( process.env.USE_AZURE_TRUSTED_SIGNING ?? '' ).trim().toLowerCase() -); - console.log( '--- :electron: Packaging AppX' ); -if ( useAzureSigning ) { - const azureSigning = await import( './azure-signing.cjs' ); - const { assertAzureSigningEnv } = azureSigning.default; - console.log( '~~~ Verifying Azure Trusted Signing env vars...' ); - try { - assertAzureSigningEnv(); - } catch ( error ) { - console.error( error instanceof Error ? error.message : error ); - process.exit( 1 ); - } -} else { - console.log( '~~~ Verifying WINDOWS_CODE_SIGNING_CERT_PASSWORD env var...' ); - if ( ! process.env.WINDOWS_CODE_SIGNING_CERT_PASSWORD ) { - console.error( 'Required env var WINDOWS_CODE_SIGNING_CERT_PASSWORD is not set!' ); - process.exit( 1 ); - } +const { assertAzureSigningEnv, getAzureSignArgs, getAzureSigningConfig } = ( + await import( './azure-signing.cjs' ) +).default; + +console.log( '~~~ Verifying Azure Trusted Signing env vars...' ); +try { + assertAzureSigningEnv(); +} catch ( error ) { + console.error( error instanceof Error ? error.message : error ); + process.exit( 1 ); } // Get architecture from environment variable, default to x64 for backward compatibility @@ -189,54 +179,40 @@ await convertToWindowsStore( { const appxOutputPathSigned = path.resolve( outPath, `${ appxName }-${ architecture }-signed` ); console.log( `~~~ Creating signed .appx for local testing at ${ appxOutputPathSigned }...` ); -if ( useAzureSigning ) { - const sideloadPublisher = - 'CN=Automattic Inc., O=Automattic Inc., L=San Francisco, S=California, C=US'; +const sideloadPublisher = + 'CN=Automattic Inc., O=Automattic Inc., L=San Francisco, S=California, C=US'; - // Build unsigned, then sign with Azure Trusted Signing via signtool. - await convertToWindowsStore( { - ...sharedOptions, - publisher: sideloadPublisher, - devCert: 'nil', - outputDirectory: appxOutputPathSigned, - } ); +// Build unsigned, then sign with Azure Trusted Signing via signtool. +await convertToWindowsStore( { + ...sharedOptions, + publisher: sideloadPublisher, + devCert: 'nil', + outputDirectory: appxOutputPathSigned, +} ); - console.log( '~~~ Signing sideload .appx with Azure Trusted Signing...' ); - const azureSigning = await import( './azure-signing.cjs' ); - const { getAzureSignArgs, getAzureSigningConfig } = azureSigning.default; - const appxFiles = ( await fs.readdir( appxOutputPathSigned ) ).filter( ( f ) => - f.endsWith( '.appx' ) - ); - if ( appxFiles.length === 0 ) { - console.error( 'No .appx file found to sign!' ); - process.exit( 1 ); - } +console.log( '~~~ Signing sideload .appx with Azure Trusted Signing...' ); +const appxFiles = ( await fs.readdir( appxOutputPathSigned ) ).filter( ( f ) => + f.endsWith( '.appx' ) +); +if ( appxFiles.length === 0 ) { + console.error( 'No .appx file found to sign!' ); + process.exit( 1 ); +} - for ( const appxFile of appxFiles ) { - const appxPath = path.join( appxOutputPathSigned, appxFile ); - console.log( `Signing ${ appxPath }...` ); - const { signtoolPath } = getAzureSigningConfig(); - execFileSync( signtoolPath, getAzureSignArgs( appxPath ), { - stdio: 'inherit', - } ); - console.log( `Signed ${ appxFile } successfully.` ); - - // Rename to remove misleading "unsigned" from the filename - const renamedFile = appxFile.replace( ' unsigned', '' ); - if ( renamedFile !== appxFile ) { - const renamedPath = path.join( appxOutputPathSigned, renamedFile ); - await fs.rename( appxPath, renamedPath ); - console.log( `Renamed to ${ renamedFile }` ); - } - } -} else { - // PFX certificate signing - await convertToWindowsStore( { - ...sharedOptions, - publisher: - 'CN="Automattic, Inc.", O="Automattic, Inc.", S=California, C=US', - devCert: 'certificate.pfx', - certPass: process.env.WINDOWS_CODE_SIGNING_CERT_PASSWORD, - outputDirectory: appxOutputPathSigned, +for ( const appxFile of appxFiles ) { + const appxPath = path.join( appxOutputPathSigned, appxFile ); + console.log( `Signing ${ appxPath }...` ); + const { signtoolPath } = getAzureSigningConfig(); + execFileSync( signtoolPath, getAzureSignArgs( appxPath ), { + stdio: 'inherit', } ); + console.log( `Signed ${ appxFile } successfully.` ); + + // Rename to remove misleading "unsigned" from the filename + const renamedFile = appxFile.replace( ' unsigned', '' ); + if ( renamedFile !== appxFile ) { + const renamedPath = path.join( appxOutputPathSigned, renamedFile ); + await fs.rename( appxPath, renamedPath ); + console.log( `Renamed to ${ renamedFile }` ); + } } From 7dea0c60d2c26600fcd6e7c8b6e5810143c58b6d Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Fri, 29 May 2026 15:48:58 +1000 Subject: [PATCH 2/2] Keep USE_AZURE gate, drop only PFX fallback `USE_AZURE_TRUSTED_SIGNING` is the signal that a build should be signed, not a redundant flag: it is set on the `make` build jobs and left unset on the Windows E2E job, which runs `electron-forge package` to produce an unsigned bundle. Removing it (and gating signing on `CI && win32` instead) would throw at config import on that E2E job and would leave the real build jobs producing unsigned installers. Retain the env var and the gate; remove only the dead PFX fallback branches. Surfaced by Copilot review on the PR. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- .buildkite/commands/build-for-windows.ps1 | 11 +++++++---- .buildkite/pipeline.yml | 2 ++ .buildkite/release-build-and-distribute.yml | 2 ++ apps/studio/windowsSign.ts | 20 +++++++++++++------- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/.buildkite/commands/build-for-windows.ps1 b/.buildkite/commands/build-for-windows.ps1 index 8abfcf2704..56d741c065 100644 --- a/.buildkite/commands/build-for-windows.ps1 +++ b/.buildkite/commands/build-for-windows.ps1 @@ -28,10 +28,13 @@ if ($Architecture -notin $VALID_ARCHITECTURES) { Exit 1 } -Write-Host "--- :lock: Setting up Azure Trusted Signing" -$setupScript = (Get-Command setup_azure_trusted_signing.ps1 -ErrorAction Stop).Source -& $setupScript -If ($LastExitCode -ne 0) { Exit $LastExitCode } +$useAzureTrustedSigning = "$($env:USE_AZURE_TRUSTED_SIGNING)".Trim().ToLower() +If (@('1', 'true') -contains $useAzureTrustedSigning) { + Write-Host "--- :lock: Setting up Azure Trusted Signing" + $setupScript = (Get-Command setup_azure_trusted_signing.ps1 -ErrorAction Stop).Source + & $setupScript + If ($LastExitCode -ne 0) { Exit $LastExitCode } +} Write-Host "--- :npm: Installing Node dependencies" bash .buildkite/commands/install-node-dependencies.sh diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 1771fd4651..9bdeafbaee 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -200,6 +200,8 @@ steps: - label: 🔨 Windows Dev Build - {{matrix}} agents: queue: windows + env: + USE_AZURE_TRUSTED_SIGNING: 1 command: powershell -File .buildkite/commands/build-for-windows.ps1 -BuildType dev -Architecture {{matrix}} artifact_paths: - apps\studio\out\**\studio-setup.exe diff --git a/.buildkite/release-build-and-distribute.yml b/.buildkite/release-build-and-distribute.yml index 3e0340dd19..742c55b4f3 100644 --- a/.buildkite/release-build-and-distribute.yml +++ b/.buildkite/release-build-and-distribute.yml @@ -68,6 +68,8 @@ steps: - label: 🔨 Windows Release Build - {{matrix}} agents: queue: windows + env: + USE_AZURE_TRUSTED_SIGNING: 1 command: | bash .buildkite/commands/checkout-release-branch.sh "${RELEASE_VERSION}" powershell -File .buildkite/commands/build-for-windows.ps1 -BuildType release -Architecture {{matrix}} diff --git a/apps/studio/windowsSign.ts b/apps/studio/windowsSign.ts index fcf6885316..1e51614699 100644 --- a/apps/studio/windowsSign.ts +++ b/apps/studio/windowsSign.ts @@ -7,19 +7,25 @@ import type { WindowsSignOptions } from '@electron/packager'; // dual-signs (SHA1 + SHA256), but Azure only supports SHA256. // The hook calls signtool directly with SHA256-only parameters. // -// Only signs on Windows CI. This config is loaded by Forge on every platform, -// so non-Windows builds (and local Windows builds) get undefined — returning -// the Azure config unconditionally would throw on Mac/Linux CI, where the -// Azure env vars are absent. +// Gated on USE_AZURE_TRUSTED_SIGNING, which the signed-build jobs set (see +// build-for-windows.ps1). Package-only jobs — e.g. the Windows E2E run, which +// uses `electron-forge package` and never signs — leave it unset and get +// undefined, so this config (loaded by Forge on every build) stays inert +// there. The throw fires only for a build that asked to sign but is missing +// its Azure env, where signtool would otherwise fail with an opaque exit code. function getWindowsSign(): WindowsSignOptions | undefined { - if ( ! process.env.CI || process.platform !== 'win32' ) { + const signWithAzure = [ '1', 'true' ].includes( + ( process.env.USE_AZURE_TRUSTED_SIGNING ?? '' ).trim().toLowerCase() + ); + + if ( ! signWithAzure ) { return undefined; } if ( ! process.env.AZURE_CODE_SIGNING_DLIB || ! process.env.AZURE_METADATA_JSON || ! process.env.SIGNTOOL_PATH ) { throw new Error( - 'Windows CI build is missing Azure Trusted Signing env vars ' + - '(AZURE_CODE_SIGNING_DLIB, AZURE_METADATA_JSON, SIGNTOOL_PATH). ' + + 'USE_AZURE_TRUSTED_SIGNING is set but Azure signing env vars ' + + '(AZURE_CODE_SIGNING_DLIB, AZURE_METADATA_JSON, SIGNTOOL_PATH) are missing. ' + 'Did setup_azure_trusted_signing.ps1 run?' ); }