diff --git a/.github/workflows/windows-signing-rehearsal.yml b/.github/workflows/windows-signing-rehearsal.yml index a42a93504b6..15d5da2f8ac 100644 --- a/.github/workflows/windows-signing-rehearsal.yml +++ b/.github/workflows/windows-signing-rehearsal.yml @@ -17,7 +17,19 @@ on: default: windows-inner-binaries-zip jobs: + # Ship the same qualified template as release-cut so the reseal over SignPath-rewritten binaries is rehearsed too. + orcad-template: + permissions: + contents: read + actions: read + uses: ./.github/workflows/qualified-orcad-template.yml + with: + ref: ${{ github.sha }} + # SignPath origin verification covers reusable-workflow jobs, so they stay GitHub-hosted. + fast_runners: false + rehearse: + needs: orcad-template # Why: SignPath origin verification requires GitHub-hosted runners, and # windows-2022 matches the real release job's build environment. runs-on: windows-2022 @@ -25,6 +37,8 @@ jobs: permissions: actions: read contents: read + env: + ORCA_REQUIRE_ORCAD_TEMPLATE: '1' steps: - name: Checkout @@ -79,6 +93,13 @@ jobs: env: NODE_OPTIONS: --max-old-space-size=4096 + # After the app build so nothing that cleans out/ can drop it; electron-builder ships it. + - name: Download the orcad deployment template + uses: actions/download-artifact@v8 + with: + name: orcad-template + path: out/orcad-template + # Build full --win: --dir never creates the NSIS uninstaller needed for MDE evidence. - name: Package Windows app and export the NSIS uninstaller shell: pwsh @@ -111,6 +132,11 @@ jobs: Where-Object { $_.Extension -in '.exe', '.dll', '.node' } | ForEach-Object { $relative = [System.IO.Path]::GetRelativePath($root, $_.FullName) + # The orcad template's Linux/macOS addons are data for SSH hosts, not PE files. + if ($relative -match '^resources[\\/]orcad-template[\\/]targets[\\/](?!win32-)') { + $skipped.Add("$relative ") + return + } $signature = Get-AuthenticodeSignature -FilePath $_.FullName if ($signature.Status -eq 'Valid') { $skipped.Add("$relative ") @@ -214,6 +240,10 @@ jobs: throw "Signed inner artifact did not round-trip cleanly ($($failures.Count) failures)." } + # Recompute template checksums over the signed bytes, then verify the template as release-cut does. + - name: Reseal the orcad template over its signed binaries + run: node config/scripts/packaged-orcad-template.cjs --reseal-signed dist/win-unpacked inner-signing-list.txt + - name: Restore signed uninstaller for the installer rebuild shell: pwsh env: diff --git a/config/scripts/windows-signing-rehearsal-orcad-template.test.mjs b/config/scripts/windows-signing-rehearsal-orcad-template.test.mjs new file mode 100644 index 00000000000..e1328094cfd --- /dev/null +++ b/config/scripts/windows-signing-rehearsal-orcad-template.test.mjs @@ -0,0 +1,45 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' +import { parse } from 'yaml' + +// The rehearsal must sign and reseal the same template payload release-cut ships, or Windows reseal regressions only surface in a real release. +function readWorkflow(name) { + return parse(readFileSync(new URL(`../../.github/workflows/${name}`, import.meta.url), 'utf8')) +} + +const RESEAL_STEP = 'Reseal the orcad template over its signed binaries' +const release = readWorkflow('release-cut.yml').jobs.build +const { jobs } = readWorkflow('windows-signing-rehearsal.yml') +const rehearse = jobs.rehearse +const stepIndex = (name) => rehearse.steps.findIndex((step) => step.name === name) + +describe('Windows signing rehearsal ships the orcad template', () => { + it('packages the qualified template built from the dispatched commit', () => { + expect(jobs['orcad-template'].uses).toBe('./.github/workflows/qualified-orcad-template.yml') + expect(jobs['orcad-template'].with).toEqual({ ref: '${{ github.sha }}', fast_runners: false }) + expect(rehearse.needs).toBe('orcad-template') + expect(rehearse.env.ORCA_REQUIRE_ORCAD_TEMPLATE).toBe('1') + const download = stepIndex('Download the orcad deployment template') + expect(download).toBeGreaterThan(stepIndex('Build app')) + expect(download).toBeLessThan(stepIndex('Package Windows app and export the NSIS uninstaller')) + expect(rehearse.steps[download].with).toEqual({ + name: 'orcad-template', + path: 'out/orcad-template' + }) + }) + + it('reseals the signed template exactly like release-cut, before the installer rebuild', () => { + const releaseReseal = release.steps.find((step) => step.name === RESEAL_STEP) + const reseal = stepIndex(RESEAL_STEP) + expect(rehearse.steps[reseal].run).toBe(releaseReseal.run) + expect(reseal).toBeGreaterThan(stepIndex('Restore signed inner binaries into unpacked app')) + expect(reseal).toBeLessThan(stepIndex('Build NSIS installer from signed unpacked app')) + }) + + it('keeps non-Windows template addons out of SignPath staging', () => { + const filter = "'^resources[\\\\/]orcad-template[\\\\/]targets[\\\\/](?!win32-)'" + const staging = (job) => job.steps.find((step) => step.id === 'stage-inner').run + expect(staging(release)).toContain(filter) + expect(staging(rehearse)).toContain(filter) + }) +})