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"