From b22db5dbd4c38985ba93f796787ef42a757c1815 Mon Sep 17 00:00:00 2001 From: Felix Rieseberg Date: Thu, 3 Sep 2026 18:09:28 -0700 Subject: [PATCH 1/2] build(win32): sign with Azure Trusted Signing via OIDC Replace the DigiCert KeyLocker flow with Azure Trusted Signing. The release workflow logs in to Azure with OIDC, installs the Trusted Signing dlib and picks the newest Windows SDK signtool; forge.config.ts turns those into @electron/windows-sign options shared by the Squirrel and MSIX makers. The MSIX publisher is updated to match the Trusted Signing certificate subject. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/release.yml | 57 +++++++++--------- forge.config.ts | 105 ++++++++++++++++++++++++++++++---- package.json | 1 + yarn.lock | 1 + 4 files changed, 128 insertions(+), 36 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df291560e9..019571ca06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,8 @@ jobs: permissions: actions: write contents: read + # Required for the OIDC login to Azure used for Windows code signing + id-token: write environment: release steps: - run: git config --global core.autocrlf input @@ -58,30 +60,32 @@ jobs: MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }} MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} run: chmod +x tools/add-macos-cert.sh && . ./tools/add-macos-cert.sh - - name: Signing Manager Setup (Windows) - if: ${{ startsWith(matrix.os, 'windows-') }} - uses: digicert/code-signing-software-trust-action@fae23a455ba4bde62b64fd7cb2f81ade788f5a95 # v1.2.1 - - name: Write authentication cert to disk (Windows) + - name: Azure login (Windows) if: ${{ startsWith(matrix.os, 'windows-') }} - shell: bash - env: - SM_CLIENT_CERT_P12_BASE64: ${{ secrets.SM_CLIENT_CERT_P12_BASE64 }} - run: | - echo "$SM_CLIENT_CERT_P12_BASE64" | base64 --decode > /d/cert.p12 - echo "SM_CLIENT_CERT_FILE=D:\\cert.p12" >> "$GITHUB_ENV" - - name: Sync cert (Windows) + uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3.0.2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Install Azure Trusted Signing client (Windows) if: ${{ startsWith(matrix.os, 'windows-') }} shell: pwsh - env: - CERT_FINGERPRINT: ${{ secrets.CERT_FINGERPRINT }} - KEYPAIR_ALIAS: ${{ secrets.KEYPAIR_ALIAS }} - SM_API_KEY: ${{ secrets.SM_API_KEY }} - SM_CLIENT_CERT_FILE: ${{ env.SM_CLIENT_CERT_FILE }} - SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} - SM_HOST: ${{ secrets.SM_HOST }} run: | - smksp_registrar list - smctl windows certsync --keypair-alias=$env:KEYPAIR_ALIAS + $clientDir = Join-Path $env:RUNNER_TEMP 'trusted-signing' + nuget install Microsoft.Trusted.Signing.Client -Version 1.0.95 -x -OutputDirectory $clientDir + $dlib = Join-Path $clientDir 'Microsoft.Trusted.Signing.Client\bin\x64\Azure.CodeSigning.Dlib.dll' + if (-not (Test-Path $dlib)) { throw "Azure.CodeSigning.Dlib.dll not found at $dlib" } + + # Trusted Signing needs signtool.exe from Windows SDK 10.0.22621.755 or + # later; pick the newest SDK installed on the runner. + $signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.*\x64\signtool.exe" | + Sort-Object { [version]$_.Directory.Parent.Name } -Descending | + Select-Object -First 1 + if (-not $signtool) { throw 'No Windows SDK signtool.exe found' } + Write-Host "Using signtool: $($signtool.FullName)" + + "AZURE_CODE_SIGNING_DLIB=$dlib" >> $env:GITHUB_ENV + "WINDOWS_SIGNTOOL_PATH=$($signtool.FullName)" >> $env:GITHUB_ENV - name: Build (macOS) if: ${{ startsWith(matrix.os, 'macos-') }} env: @@ -91,12 +95,13 @@ jobs: - name: Build (Windows) if: ${{ startsWith(matrix.os, 'windows-') }} env: - CERT_FINGERPRINT: ${{ secrets.CERT_FINGERPRINT }} - KEYPAIR_ALIAS: ${{ secrets.KEYPAIR_ALIAS }} - SM_API_KEY: ${{ secrets.SM_API_KEY }} - SM_CLIENT_CERT_FILE: ${{ env.SM_CLIENT_CERT_FILE }} - SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} - SM_HOST: ${{ secrets.SM_HOST }} + # AZURE_CODE_SIGNING_DLIB and WINDOWS_SIGNTOOL_PATH come from the + # previous step via GITHUB_ENV. The values below identify the Trusted + # Signing account and are not secrets; the endpoint must match the + # region the account was created in. + AZURE_CODE_SIGNING_ENDPOINT: https://eus.codesigning.azure.net + AZURE_CODE_SIGNING_ACCOUNT_NAME: OpenJS-CodeSigning + AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME: Electron run: yarn run publish --arch=${{ matrix.arch }} --dry-run # zizmor: ignore[use-trusted-publishing] - name: Build (Linux) if: ${{ startsWith(matrix.os, 'ubuntu-') }} diff --git a/forge.config.ts b/forge.config.ts index 4c22d4e675..002128bae9 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -1,6 +1,9 @@ +import * as fs from 'node:fs'; +import * as os from 'node:os'; import * as path from 'node:path'; import { FuseV1Options, FuseVersion } from '@electron/fuses'; +import type { SignToolOptions } from '@electron/windows-sign'; import { MakerMSIX } from '@electron-forge/maker-msix'; import { FusesPlugin } from '@electron-forge/plugin-fuses'; import type { ForgeConfig } from '@electron-forge/shared-types'; @@ -26,6 +29,93 @@ const commonLinuxConfig = { const requirements = path.resolve(__dirname, 'tools/certs/requirements.txt'); +/** + * Windows code signing through Azure Trusted Signing. + * + * Authentication is not handled here. In CI, `azure/login` performs an OIDC + * login with the Azure CLI, and the Trusted Signing dlib then picks up that + * session through `AzureCliCredential`. This function only tells signtool + * where the dlib lives and which account and certificate profile to use. + * + * Returns `undefined` when none of the Azure variables are set so that local + * and CI builds produce unsigned artifacts, as before. + */ +function getWindowsSignOptions(): SignToolOptions | undefined { + const { + AZURE_CODE_SIGNING_DLIB: dlib, + AZURE_CODE_SIGNING_ENDPOINT: endpoint, + AZURE_CODE_SIGNING_ACCOUNT_NAME: accountName, + AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME: certificateProfileName, + WINDOWS_SIGNTOOL_PATH: signToolPath, + } = process.env; + + if (!dlib && !endpoint && !accountName && !certificateProfileName) { + return undefined; + } + + if (!dlib || !endpoint || !accountName || !certificateProfileName) { + throw new Error( + 'Azure Trusted Signing is only partially configured. Set all of ' + + 'AZURE_CODE_SIGNING_DLIB, AZURE_CODE_SIGNING_ENDPOINT, ' + + 'AZURE_CODE_SIGNING_ACCOUNT_NAME and ' + + 'AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME, or none of them.', + ); + } + + if (!signToolPath) { + // The signtool.exe vendored by @electron/windows-sign predates /dlib + // support. Trusted Signing needs one from Windows SDK 10.0.22621.755+. + throw new Error( + 'Azure Trusted Signing needs a recent signtool.exe. Set ' + + 'WINDOWS_SIGNTOOL_PATH to one from Windows SDK 10.0.22621.755 or later.', + ); + } + + const metadataPath = path.join( + os.tmpdir(), + 'electron-fiddle-trusted-signing-metadata.json', + ); + + fs.writeFileSync( + metadataPath, + JSON.stringify( + { + Endpoint: endpoint, + CodeSigningAccountName: accountName, + CertificateProfileName: certificateProfileName, + // `azure/login` leaves us with an Azure CLI session. Skip the other + // credential providers DefaultAzureCredential would otherwise probe, + // some of which (managed identity) time out slowly on GitHub runners. + ExcludeCredentials: [ + 'ManagedIdentityCredential', + 'WorkloadIdentityCredential', + 'SharedTokenCacheCredential', + 'VisualStudioCredential', + 'VisualStudioCodeCredential', + 'AzurePowerShellCredential', + 'AzureDeveloperCliCredential', + 'InteractiveBrowserCredential', + ], + }, + null, + 2, + ), + ); + + return { + signToolPath, + // Passed as an array so paths with spaces survive intact. + signWithParams: ['/dlib', dlib, '/dmdf', metadataPath], + timestampServer: 'http://timestamp.acs.microsoft.com', + // Trusted Signing certificates are SHA-256 only; no SHA-1 dual signing. + hashes: ['sha256'] as SignToolOptions['hashes'], + // Certificate selection is done by the dlib, not by signtool's `/a`. + automaticallySelectCertificate: false, + }; +} + +const windowsSignOptions = getWindowsSignOptions(); + const config: ForgeConfig = { hooks: { generateAssets: async () => { @@ -137,15 +227,15 @@ const config: ForgeConfig = { noMsi: true, setupExe: `electron-fiddle-${version}-win32-${arch}-setup.exe`, setupIcon: path.resolve(iconDir, 'fiddle.ico'), - signWithParams: process.env.CERT_FINGERPRINT - ? `/sha1 ${process.env.CERT_FINGERPRINT} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256` - : undefined, + windowsSign: windowsSignOptions, }), }, new MakerMSIX({ manifestVariables: { + // Must match the subject of the Azure Trusted Signing certificate + // exactly, or signtool refuses to sign the package. publisher: - 'CN=OpenJS Foundation, OU=Electron, O=OpenJS Foundation, L=San Francisco, S=California, C=US, SERIALNUMBER=5579593, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US', + 'CN=OpenJS Foundation, O=OpenJS Foundation, L=San Francisco, S=California, C=US', publisherDisplayName: 'OpenJS Foundation', packageIdentity: 'ElectronCommunity.ElectronFiddle', appExecutable: 'electron-fiddle.exe', @@ -153,12 +243,7 @@ const config: ForgeConfig = { appDisplayName: 'Electron Fiddle', packageDescription: packageJson.description, }, - windowsSignOptions: process.env.CERT_FINGERPRINT - ? { - signWithParams: `/sha1 ${process.env.CERT_FINGERPRINT}`, - hashes: ['sha256'] as any, - } - : undefined, + windowsSignOptions, }), { name: '@electron-forge/maker-zip', diff --git a/package.json b/package.json index 9fb61709d3..068cb5dca8 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@electron/devtron": "^2.1.1", "@electron/fuses": "^2.1.1", "@electron/lint-roller": "^3.1.3", + "@electron/windows-sign": "^1.2.2", "@reforged/maker-appimage": "^5.1.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", diff --git a/yarn.lock b/yarn.lock index 95269179da..efb66711b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5328,6 +5328,7 @@ __metadata: "@electron/fiddle-core": "npm:^2.2.0" "@electron/fuses": "npm:^2.1.1" "@electron/lint-roller": "npm:^3.1.3" + "@electron/windows-sign": "npm:^1.2.2" "@octokit/rest": "npm:^22.0.1" "@reforged/maker-appimage": "npm:^5.1.0" "@sentry/electron": "npm:^7.12.0" From a072cb5ce6ca2616f55628eb76ee1dce1158d07c Mon Sep 17 00:00:00 2001 From: Felix Rieseberg Date: Sat, 5 Sep 2026 17:26:10 -0700 Subject: [PATCH 2/2] build(win32): scope OIDC to a Windows-only job and drop windows-sign dep Only the Windows build needs to request GitHub ID tokens for Azure login, so it now runs in its own job with id-token: write. The sign options type is derived from Forge instead of importing @electron/windows-sign. --- .github/workflows/release.yml | 82 ++++++++++++++++++++++++----------- forge.config.ts | 11 +++-- package.json | 1 - yarn.lock | 1 - 4 files changed, 64 insertions(+), 31 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 019571ca06..24b3998e98 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,6 @@ jobs: os: - macos-latest-large - ubuntu-latest - - windows-latest arch: - x64 include: @@ -26,14 +25,10 @@ jobs: arch: armv7l - os: ubuntu-24.04-arm arch: arm64 - - os: windows-latest - arch: ia32 runs-on: "${{ matrix.os }}" permissions: actions: write contents: read - # Required for the OIDC login to Azure used for Windows code signing - id-token: write environment: release steps: - run: git config --global core.autocrlf input @@ -60,15 +55,65 @@ jobs: MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }} MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} run: chmod +x tools/add-macos-cert.sh && . ./tools/add-macos-cert.sh - - name: Azure login (Windows) - if: ${{ startsWith(matrix.os, 'windows-') }} + - name: Build (macOS) + if: ${{ startsWith(matrix.os, 'macos-') }} + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + run: yarn run publish --arch=${{ matrix.arch }} --dry-run # zizmor: ignore[use-trusted-publishing] + - name: Build (Linux) + if: ${{ startsWith(matrix.os, 'ubuntu-') }} + run: yarn run publish --arch=${{ matrix.arch }} --dry-run # zizmor: ignore[use-trusted-publishing] + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build-artifacts-${{ matrix.os }}-${{ matrix.arch }} + path: out + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ startsWith(matrix.os, 'ubuntu-') && matrix.arch == 'x64' }} + with: + name: webpack-source-maps + path: .webpack + include-hidden-files: true + + build-windows: + name: Build (Windows) + strategy: + fail-fast: false + matrix: + arch: + - x64 + - ia32 + runs-on: windows-latest + permissions: + actions: write + contents: read + # Required for the OIDC login to Azure used for code signing. Kept in + # its own job so the macOS and Linux runners can't request ID tokens. + id-token: write + environment: release + steps: + - run: git config --global core.autocrlf input + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + package-manager-cache: false + - run: yarn install --immutable + - run: yarn run contributors + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: yarn run electron-releases + - name: Azure login uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3.0.2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - name: Install Azure Trusted Signing client (Windows) - if: ${{ startsWith(matrix.os, 'windows-') }} + - name: Install Azure Trusted Signing client shell: pwsh run: | $clientDir = Join-Path $env:RUNNER_TEMP 'trusted-signing' @@ -86,14 +131,7 @@ jobs: "AZURE_CODE_SIGNING_DLIB=$dlib" >> $env:GITHUB_ENV "WINDOWS_SIGNTOOL_PATH=$($signtool.FullName)" >> $env:GITHUB_ENV - - name: Build (macOS) - if: ${{ startsWith(matrix.os, 'macos-') }} - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - run: yarn run publish --arch=${{ matrix.arch }} --dry-run # zizmor: ignore[use-trusted-publishing] - name: Build (Windows) - if: ${{ startsWith(matrix.os, 'windows-') }} env: # AZURE_CODE_SIGNING_DLIB and WINDOWS_SIGNTOOL_PATH come from the # previous step via GITHUB_ENV. The values below identify the Trusted @@ -103,19 +141,10 @@ jobs: AZURE_CODE_SIGNING_ACCOUNT_NAME: OpenJS-CodeSigning AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME: Electron run: yarn run publish --arch=${{ matrix.arch }} --dry-run # zizmor: ignore[use-trusted-publishing] - - name: Build (Linux) - if: ${{ startsWith(matrix.os, 'ubuntu-') }} - run: yarn run publish --arch=${{ matrix.arch }} --dry-run # zizmor: ignore[use-trusted-publishing] - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: build-artifacts-${{ matrix.os }}-${{ matrix.arch }} + name: build-artifacts-windows-latest-${{ matrix.arch }} path: out - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: ${{ startsWith(matrix.os, 'ubuntu-') && matrix.arch == 'x64' }} - with: - name: webpack-source-maps - path: .webpack - include-hidden-files: true test: permissions: @@ -127,6 +156,7 @@ jobs: runs-on: ubuntu-latest needs: - build + - build-windows - test environment: release permissions: diff --git a/forge.config.ts b/forge.config.ts index 002128bae9..c6b7d186b0 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -3,7 +3,6 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { FuseV1Options, FuseVersion } from '@electron/fuses'; -import type { SignToolOptions } from '@electron/windows-sign'; import { MakerMSIX } from '@electron-forge/maker-msix'; import { FusesPlugin } from '@electron-forge/plugin-fuses'; import type { ForgeConfig } from '@electron-forge/shared-types'; @@ -40,7 +39,13 @@ const requirements = path.resolve(__dirname, 'tools/certs/requirements.txt'); * Returns `undefined` when none of the Azure variables are set so that local * and CI builds produce unsigned artifacts, as before. */ -function getWindowsSignOptions(): SignToolOptions | undefined { +// Derived from Forge rather than imported from `@electron/windows-sign`, so the +// type always matches the version Forge itself depends on. +type WindowsSignOptions = NonNullable< + MakerMSIX['config']['windowsSignOptions'] +>; + +function getWindowsSignOptions(): WindowsSignOptions | undefined { const { AZURE_CODE_SIGNING_DLIB: dlib, AZURE_CODE_SIGNING_ENDPOINT: endpoint, @@ -108,7 +113,7 @@ function getWindowsSignOptions(): SignToolOptions | undefined { signWithParams: ['/dlib', dlib, '/dmdf', metadataPath], timestampServer: 'http://timestamp.acs.microsoft.com', // Trusted Signing certificates are SHA-256 only; no SHA-1 dual signing. - hashes: ['sha256'] as SignToolOptions['hashes'], + hashes: ['sha256'] as WindowsSignOptions['hashes'], // Certificate selection is done by the dlib, not by signtool's `/a`. automaticallySelectCertificate: false, }; diff --git a/package.json b/package.json index 068cb5dca8..9fb61709d3 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,6 @@ "@electron/devtron": "^2.1.1", "@electron/fuses": "^2.1.1", "@electron/lint-roller": "^3.1.3", - "@electron/windows-sign": "^1.2.2", "@reforged/maker-appimage": "^5.1.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", diff --git a/yarn.lock b/yarn.lock index efb66711b6..95269179da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5328,7 +5328,6 @@ __metadata: "@electron/fiddle-core": "npm:^2.2.0" "@electron/fuses": "npm:^2.1.1" "@electron/lint-roller": "npm:^3.1.3" - "@electron/windows-sign": "npm:^1.2.2" "@octokit/rest": "npm:^22.0.1" "@reforged/maker-appimage": "npm:^5.1.0" "@sentry/electron": "npm:^7.12.0"