Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 31 additions & 26 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines 32 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 id-token: write is granted at the job level, so it applies to every matrix runner (macOS, Linux, Windows) even though only the Windows leg needs OIDC for Azure login; before this change no runner in this workflow could mint an OIDC token. Fix: scope OIDC access to only the Windows signing work, e.g. split Windows into its own job with its own permissions block so macOS/Linux runners (which install arbitrary npm deps and run notarization scripts) don't gain the ability to request GitHub ID tokens.

Extended reasoning...

GitHub Actions permissions are job-scoped, not step-scoped, so adding id-token: write to the shared build job (matrix includes macos-latest-large, macos-latest-xlarge, ubuntu-latest, ubuntu-24.04-arm, windows-latest) exposes ACTIONS_ID_TOKEN_REQUEST_URL/TOKEN to every step on every OS, not just the new azure/login step guarded by if: startsWith(matrix.os, 'windows-'). A compromised transitive dependency pulled in during yarn install or any script run on the macOS/Linux runners could now request and exfiltrate a GitHub OIDC token (usable against any relying party trusting this repo's OIDC issuer), a capability that did not exist on any runner before this PR.

Verification: normal (security-relevant, newly exposed by this change): The diff adds id-token: write to the permissions: block of the single build job (release.yml lines 32-36). GitHub Actions permissions are job-scoped, not step-scoped, and this job runs a matrix spanning macos-latest-large, macos-latest-xlarge, ubuntu-latest, ubuntu-24.04-arm, and windows-latest (lines 15-33). Every one of those…

environment: release
steps:
- run: git config --global core.autocrlf input
Expand All @@ -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:
Expand All @@ -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-') }}
Expand Down
105 changes: 95 additions & 10 deletions forge.config.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 () => {
Expand Down Expand Up @@ -137,28 +227,23 @@ 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',
packageDisplayName: 'Electron Fiddle',
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',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down