diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml index e68ce4f4..2eed5530 100644 --- a/.github/workflows/electron-build.yml +++ b/.github/workflows/electron-build.yml @@ -243,8 +243,45 @@ jobs: run: pnpm install --frozen-lockfile working-directory: apps/x + - name: Setup Azure Trusted Signing + shell: pwsh + run: | + # Stage everything under a path WITHOUT spaces: @electron/windows-sign + # splits signWithParams on spaces, so the /dlib and /dmdf paths must be + # space-free. + $dir = "C:\azsign" + New-Item -ItemType Directory -Force -Path $dir | Out-Null + + # The signtool plugin that delegates signing to Azure Trusted Signing. + nuget install Microsoft.Trusted.Signing.Client -x -OutputDirectory "$dir\nuget" + $dlib = "$dir\nuget\Microsoft.Trusted.Signing.Client\bin\x64\Azure.CodeSigning.Dlib.dll" + if (-not (Test-Path $dlib)) { throw "Azure.CodeSigning.Dlib.dll not found at $dlib" } + + # Tells the dlib which account/profile to sign with. + @{ + Endpoint = "${{ secrets.AZURE_ENDPOINT }}" + CodeSigningAccountName = "${{ secrets.AZURE_CODE_SIGNING_NAME }}" + CertificateProfileName = "${{ secrets.AZURE_CERT_PROFILE_NAME }}" + } | ConvertTo-Json | Out-File "$dir\metadata.json" -Encoding utf8 + + # Newest Windows SDK signtool (the one vendored by @electron/windows-sign + # is too old to load the dlib). Copied into the space-free dir. + $signtool = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe" | + Where-Object { $_.Directory.Parent.Name -match '^\d+(\.\d+)+$' } | + Sort-Object { [version]$_.Directory.Parent.Name } | Select-Object -Last 1 + if (-not $signtool) { throw "signtool.exe not found in Windows SDK" } + Copy-Item $signtool.FullName "$dir\signtool.exe" + & "$dir\signtool.exe" | Select-Object -First 3 + + echo "AZURE_CODE_SIGNING_DLIB=$dlib" >> $env:GITHUB_ENV + echo "AZURE_METADATA_JSON=$dir\metadata.json" >> $env:GITHUB_ENV + echo "SIGNTOOL_PATH=$dir\signtool.exe" >> $env:GITHUB_ENV + - name: Build electron app env: + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} VITE_PUBLIC_POSTHOG_KEY: ${{ secrets.VITE_PUBLIC_POSTHOG_KEY }} VITE_PUBLIC_POSTHOG_HOST: ${{ secrets.VITE_PUBLIC_POSTHOG_HOST }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/apps/x/apps/main/forge.config.cjs b/apps/x/apps/main/forge.config.cjs index 581206a8..cc6c4dd6 100644 --- a/apps/x/apps/main/forge.config.cjs +++ b/apps/x/apps/main/forge.config.cjs @@ -12,6 +12,28 @@ const pkg = require('./package.json'); const SKIP_PACMAN = process.env.ROWBOAT_SKIP_PACMAN === '1'; const SKIP_CODE_SIGNING = process.env.ROWBOAT_SKIP_CODE_SIGNING === '1'; +// Windows code signing via Azure Trusted Signing — CI-only. The GitHub workflow +// downloads the Azure dlib, writes metadata.json, and exports these env vars; +// when they're absent (local builds, mac/linux jobs) Windows signing is skipped. +// signtool loads Azure.CodeSigning.Dlib.dll (/dlib), which reads metadata.json +// (/dmdf) for the endpoint/account/profile and authenticates via +// AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_CLIENT_SECRET. +// NOTE: @electron/windows-sign splits signWithParams on spaces, so the dlib and +// metadata paths must not contain spaces (the workflow stages them in C:\azsign). +const WINDOWS_SIGN = + !SKIP_CODE_SIGNING && + process.env.AZURE_CODE_SIGNING_DLIB && + process.env.AZURE_METADATA_JSON + ? { + // The signtool vendored by @electron/windows-sign is too old for the + // Azure dlib; the workflow points this at the Windows SDK's signtool. + ...(process.env.SIGNTOOL_PATH ? { signToolPath: process.env.SIGNTOOL_PATH } : {}), + signWithParams: `/v /debug /dlib ${process.env.AZURE_CODE_SIGNING_DLIB} /dmdf ${process.env.AZURE_METADATA_JSON}`, + timestampServer: 'http://timestamp.acs.microsoft.com', + hashes: ['sha256'], + } + : undefined; + // Stage the ACP coding-adapters (@agentclientprotocol/*-acp) and their full // production dependency closure into the packaged app. // @@ -202,6 +224,9 @@ module.exports = { NSAudioCaptureUsageDescription: 'Rowboat needs access to system audio to transcribe meetings from other apps (Zoom, Meet, etc.)', NSCameraUsageDescription: 'Rowboat uses your camera in video chat mode so the assistant can see you and give feedback (e.g. pitch practice).', }, + // Signs the packaged app's executables (rowboat.exe etc.); the Squirrel + // maker below separately signs the installer it produces. + ...(WINDOWS_SIGN ? { windowsSign: WINDOWS_SIGN } : {}), ...(SKIP_CODE_SIGNING ? {} : { osxSign: { batchCodesignCalls: true, @@ -260,6 +285,9 @@ module.exports = { // GitHub release page next to setup.exe and users grab the // wrong one (it neither launches the app nor auto-updates). noMsi: true, + // Sign the Squirrel installer (setup.exe) and the binaries it + // repackages. No-op unless the CI signing env vars are set. + ...(WINDOWS_SIGN ? { windowsSign: WINDOWS_SIGN } : {}), }) }, {