Prevent unsigned Windows app releases (#6806)

* Verify Windows app executable signing

* Isolate Windows signing verifier tests

* Handle direct Windows installer extraction

---------

Co-authored-by: Neil <neil@stably.ai>
This commit is contained in:
Brennan Benson
2026-07-02 10:54:19 -07:00
committed by GitHub
co-authored by Neil
parent dbf80864fe
commit 85cec26773
4 changed files with 503 additions and 0 deletions
+45
View File
@@ -1062,6 +1062,51 @@ jobs:
}
$signature.SignerCertificate | Format-List Subject,Issuer,NotBefore,NotAfter,Thumbprint
- name: Verify signed Windows inner executable
if: matrix.platform == 'win'
shell: pwsh
env:
ORCA_WINDOWS_EXPECTED_SIGNERS: CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US;CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, ST=Delaware, C=US
run: |
# Why: SignPath must recursively sign nested PE files in the dashboard
# artifact configuration; this gate keeps unsigned app payloads out of releases.
$installer = Join-Path -Path (Get-Location) -ChildPath 'dist/orca-windows-setup.exe'
$setupExtractDir = Join-Path -Path $env:RUNNER_TEMP -ChildPath 'orca-signed-setup'
$appExtractDir = Join-Path -Path $env:RUNNER_TEMP -ChildPath 'orca-signed-app'
Remove-Item -LiteralPath $setupExtractDir, $appExtractDir -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $setupExtractDir, $appExtractDir -Force | Out-Null
& 7z x -y "-o$setupExtractDir" $installer
if ($LASTEXITCODE -ne 0) {
throw "Failed to extract signed Windows installer with 7z. Exit code: $LASTEXITCODE"
}
# Why: 7-Zip can expose electron-builder NSIS payloads either as an
# app-root tree directly or as a nested app-*.7z archive.
$innerExecutables = @(Get-ChildItem -LiteralPath $setupExtractDir -File -Filter 'Orca.exe')
if ($innerExecutables.Count -eq 0) {
$appArchives = @(Get-ChildItem -LiteralPath $setupExtractDir -Recurse -File -Filter 'app-*.7z')
if ($appArchives.Count -ne 1) {
$matches = ($appArchives | ForEach-Object { $_.FullName }) -join [Environment]::NewLine
throw "Expected app-root Orca.exe or exactly one app-*.7z payload in signed Windows installer; found $($appArchives.Count) app archive(s).$([Environment]::NewLine)$matches"
}
& 7z x -y "-o$appExtractDir" $($appArchives[0].FullName)
if ($LASTEXITCODE -ne 0) {
throw "Failed to extract signed Windows app payload with 7z. Exit code: $LASTEXITCODE"
}
$innerExecutables = @(Get-ChildItem -LiteralPath $appExtractDir -File -Filter 'Orca.exe')
}
if ($innerExecutables.Count -ne 1) {
$matches = ($innerExecutables | ForEach-Object { $_.FullName }) -join [Environment]::NewLine
throw "Expected exactly one app-root Orca.exe in signed Windows app payload; found $($innerExecutables.Count).$([Environment]::NewLine)$matches"
}
node config/scripts/verify-windows-inner-signature.mjs $($innerExecutables[0].FullName)
- name: Publish signed Windows release artifacts
if: matrix.platform == 'win'
uses: nick-fields/retry@v4
@@ -217,6 +217,53 @@ describe('Electron runtime package contract', () => {
expect(installRun).not.toMatch(/throw\s+\$_/)
})
it('verifies the SignPath-signed Windows inner executable before publishing', () => {
const releaseWorkflow = readFileSync(
join(projectDir, '.github/workflows/release-cut.yml'),
'utf8'
)
const parsedWorkflow = parse(releaseWorkflow)
const steps = parsedWorkflow.jobs.build.steps
const stepNames = steps.map((step) => step.name)
const outerVerifyIndex = stepNames.indexOf('Verify signed Windows installer')
const innerVerifyIndex = stepNames.indexOf('Verify signed Windows inner executable')
const publishIndex = stepNames.indexOf('Publish signed Windows release artifacts')
expect(outerVerifyIndex).toBeGreaterThan(-1)
expect(innerVerifyIndex).toBe(outerVerifyIndex + 1)
expect(innerVerifyIndex).toBeLessThan(publishIndex)
const innerVerifyStep = steps[innerVerifyIndex]
const innerVerifyRun = innerVerifyStep.run
expect(innerVerifyStep.if).toBe("matrix.platform == 'win'")
expect(innerVerifyStep.shell).toBe('pwsh')
expect(innerVerifyStep.env.ORCA_WINDOWS_EXPECTED_SIGNERS).toBe(
'CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US;CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, ST=Delaware, C=US'
)
expect(innerVerifyRun).toContain('& 7z x -y "-o$setupExtractDir" $installer')
expect(innerVerifyRun).toContain(
"$innerExecutables = @(Get-ChildItem -LiteralPath $setupExtractDir -File -Filter 'Orca.exe')"
)
expect(innerVerifyRun).toContain('if ($innerExecutables.Count -eq 0)')
expect(innerVerifyRun).toContain(
"$appArchives = @(Get-ChildItem -LiteralPath $setupExtractDir -Recurse -File -Filter 'app-*.7z')"
)
expect(innerVerifyRun).toContain('if ($appArchives.Count -ne 1)')
expect(innerVerifyRun).toContain('Expected app-root Orca.exe or exactly one app-*.7z')
expect(innerVerifyRun).toContain('& 7z x -y "-o$appExtractDir" $($appArchives[0].FullName)')
expect(innerVerifyRun).toContain(
"$innerExecutables = @(Get-ChildItem -LiteralPath $appExtractDir -File -Filter 'Orca.exe')"
)
expect(innerVerifyRun.indexOf("-Filter 'Orca.exe'")).toBeLessThan(
innerVerifyRun.indexOf("-Filter 'app-*.7z'")
)
expect(innerVerifyRun).toContain('if ($innerExecutables.Count -ne 1)')
expect(innerVerifyRun).toContain(
'node config/scripts/verify-windows-inner-signature.mjs $($innerExecutables[0].FullName)'
)
})
it('publishes both Linux release matrix entries', () => {
const releaseWorkflow = readFileSync(
join(projectDir, '.github/workflows/release-cut.yml'),
@@ -0,0 +1,200 @@
import { spawnSync } from 'node:child_process'
import { existsSync, statSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
export const DEFAULT_EXPECTED_SIGNER =
'CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US'
const POWERSHELL_SIGNATURE_SCRIPT = String.raw`
$ErrorActionPreference = 'Stop'
$signature = Get-AuthenticodeSignature -FilePath $args[0]
$certificate = $signature.SignerCertificate
[pscustomobject]@{
status = $signature.Status.ToString()
statusMessage = $signature.StatusMessage
signerSubject = if ($null -eq $certificate) { $null } else { $certificate.Subject }
signerIssuer = if ($null -eq $certificate) { $null } else { $certificate.Issuer }
signerThumbprint = if ($null -eq $certificate) { $null } else { $certificate.Thumbprint }
notBefore = if ($null -eq $certificate) { $null } else { $certificate.NotBefore.ToString('o') }
notAfter = if ($null -eq $certificate) { $null } else { $certificate.NotAfter.ToString('o') }
} | ConvertTo-Json -Compress
`
export function normalizeSignerSubject(subject) {
if (typeof subject !== 'string') {
return ''
}
return subject
.split(',')
.map((part) => part.trim().replace(/\s*=\s*/u, '='))
.filter(Boolean)
.join(', ')
}
export function normalizeThumbprint(thumbprint) {
if (typeof thumbprint !== 'string') {
return ''
}
return thumbprint.replace(/[^0-9a-f]/giu, '').toUpperCase()
}
export function parseExpectedSigners(value = process.env.ORCA_WINDOWS_EXPECTED_SIGNERS) {
const source = typeof value === 'string' && value.trim() !== '' ? value : DEFAULT_EXPECTED_SIGNER
return source
.split(/[\r\n;]+/u)
.map(normalizeSignerSubject)
.filter(Boolean)
}
export function parseExpectedThumbprints(value = process.env.ORCA_WINDOWS_EXPECTED_THUMBPRINTS) {
if (typeof value !== 'string' || value.trim() === '') {
return []
}
return value
.split(/[\r\n,;]+/u)
.map(normalizeThumbprint)
.filter(Boolean)
}
export function parseSignatureJson(stdout) {
const trimmed = typeof stdout === 'string' ? stdout.trim() : ''
if (trimmed === '') {
throw new Error('PowerShell did not return signature JSON.')
}
try {
return JSON.parse(trimmed)
} catch (error) {
throw new Error(`PowerShell returned malformed signature JSON: ${error.message}`)
}
}
export function classifySignature(signature, options = {}) {
const expectedSigners = options.expectedSigners ?? parseExpectedSigners()
const expectedThumbprints = options.expectedThumbprints ?? parseExpectedThumbprints()
const status = typeof signature?.status === 'string' ? signature.status : ''
const signerSubject = normalizeSignerSubject(signature?.signerSubject)
const signerThumbprint = normalizeThumbprint(signature?.signerThumbprint)
const subjectAllowed = expectedSigners.includes(signerSubject)
const thumbprintAllowed =
expectedThumbprints.length > 0 &&
signerThumbprint !== '' &&
expectedThumbprints.includes(signerThumbprint)
if (status !== 'Valid') {
return {
ok: false,
message: `Windows inner executable signature status is ${status || '<missing>'}.`,
signature
}
}
if (!subjectAllowed && !thumbprintAllowed) {
return {
ok: false,
message: `Unexpected Windows inner executable signer: ${signerSubject || '<missing>'}.`,
signature
}
}
return { ok: true, signature }
}
export function formatSignatureSummary(signature) {
return [
`Status: ${signature.status ?? '<missing>'}`,
`Subject: ${normalizeSignerSubject(signature.signerSubject) || '<missing>'}`,
`Issuer: ${signature.signerIssuer ?? '<missing>'}`,
`Thumbprint: ${normalizeThumbprint(signature.signerThumbprint) || '<missing>'}`,
`NotBefore: ${signature.notBefore ?? '<missing>'}`,
`NotAfter: ${signature.notAfter ?? '<missing>'}`
].join('\n')
}
export function validateExecutablePath(executablePath) {
if (typeof executablePath !== 'string' || executablePath.trim() === '') {
throw new Error('Usage: node config/scripts/verify-windows-inner-signature.mjs <Orca.exe>')
}
if (!existsSync(executablePath)) {
throw new Error(`Windows inner executable does not exist: ${executablePath}`)
}
if (!statSync(executablePath).isFile()) {
throw new Error(`Windows inner executable path is not a file: ${executablePath}`)
}
}
export function getPowerShellSignatureJson(executablePath, spawnSyncImpl = spawnSync) {
const result = spawnSyncImpl(
'pwsh',
[
'-NoLogo',
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Bypass',
'-Command',
POWERSHELL_SIGNATURE_SCRIPT,
executablePath
],
{ encoding: 'utf8' }
)
if (result.error) {
throw result.error
}
if (result.stderr?.trim()) {
throw new Error(`PowerShell wrote to stderr while checking signature:\n${result.stderr.trim()}`)
}
if (result.status !== 0) {
throw new Error(
`PowerShell signature check failed with exit code ${result.status ?? '<unknown>'}.`
)
}
return result.stdout
}
export function verifyWindowsInnerSignature({
executablePath,
platform = process.platform,
spawnSyncImpl = spawnSync,
expectedSigners = parseExpectedSigners(),
expectedThumbprints = parseExpectedThumbprints()
}) {
validateExecutablePath(executablePath)
if (platform !== 'win32') {
throw new Error('Windows inner executable signature verification requires Windows.')
}
const signature = parseSignatureJson(getPowerShellSignatureJson(executablePath, spawnSyncImpl))
const classification = classifySignature(signature, { expectedSigners, expectedThumbprints })
if (!classification.ok) {
throw new Error(`${classification.message}\n${formatSignatureSummary(signature)}`)
}
return signature
}
export function main(argv = process.argv.slice(2)) {
try {
const signature = verifyWindowsInnerSignature({ executablePath: argv[0] })
console.log('Verified Windows inner executable signature.')
console.log(formatSignatureSummary(signature))
} catch (error) {
console.error(error.message)
process.exitCode = 1
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main()
}
@@ -0,0 +1,211 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
DEFAULT_EXPECTED_SIGNER,
classifySignature,
getPowerShellSignatureJson,
normalizeSignerSubject,
normalizeThumbprint,
parseExpectedSigners,
parseExpectedThumbprints,
parseSignatureJson,
validateExecutablePath,
verifyWindowsInnerSignature
} from './verify-windows-inner-signature.mjs'
const validSignature = {
status: 'Valid',
statusMessage: 'Signature verified.',
signerSubject: DEFAULT_EXPECTED_SIGNER,
signerIssuer: 'CN=SignPath Foundation Root',
signerThumbprint: 'aa bb cc dd',
notBefore: '2026-01-01T00:00:00.0000000Z',
notAfter: '2027-01-01T00:00:00.0000000Z'
}
function withTempFile(callback) {
const dir = mkdtempSync(join(tmpdir(), 'orca-inner-signature-'))
const filePath = join(dir, 'Orca.exe')
writeFileSync(filePath, 'placeholder executable')
try {
return callback(filePath, dir)
} finally {
rmSync(dir, { force: true, recursive: true })
}
}
describe('verify-windows-inner-signature', () => {
const originalExpectedSigners = process.env.ORCA_WINDOWS_EXPECTED_SIGNERS
const originalExpectedThumbprints = process.env.ORCA_WINDOWS_EXPECTED_THUMBPRINTS
beforeEach(() => {
delete process.env.ORCA_WINDOWS_EXPECTED_SIGNERS
delete process.env.ORCA_WINDOWS_EXPECTED_THUMBPRINTS
})
afterEach(() => {
if (originalExpectedSigners === undefined) {
delete process.env.ORCA_WINDOWS_EXPECTED_SIGNERS
} else {
process.env.ORCA_WINDOWS_EXPECTED_SIGNERS = originalExpectedSigners
}
if (originalExpectedThumbprints === undefined) {
delete process.env.ORCA_WINDOWS_EXPECTED_THUMBPRINTS
} else {
process.env.ORCA_WINDOWS_EXPECTED_THUMBPRINTS = originalExpectedThumbprints
}
})
it('normalizes signer subjects without widening exact matching', () => {
expect(
normalizeSignerSubject(
' CN = SignPath Foundation , O=SignPath Foundation,L=Lewes,S=Delaware,C=US '
)
).toBe(DEFAULT_EXPECTED_SIGNER)
expect(
normalizeSignerSubject(
'CN=Different Signer, O=SignPath Foundation, L=Lewes, S=Delaware, C=US'
)
).not.toBe(DEFAULT_EXPECTED_SIGNER)
})
it('defaults the expected signer allowlist to SignPath Foundation', () => {
expect(parseExpectedSigners('')).toEqual([DEFAULT_EXPECTED_SIGNER])
expect(parseExpectedSigners(' ')).toEqual([DEFAULT_EXPECTED_SIGNER])
})
it('parses semicolon and newline separated signer allowlists', () => {
expect(parseExpectedSigners(`${DEFAULT_EXPECTED_SIGNER};\nCN=Backup, O=Backup`)).toEqual([
DEFAULT_EXPECTED_SIGNER,
'CN=Backup, O=Backup'
])
})
it('normalizes optional thumbprint allowlists', () => {
expect(normalizeThumbprint('aa bb:cc')).toBe('AABBCC')
expect(parseExpectedThumbprints('aa bb cc,11:22:33')).toEqual(['AABBCC', '112233'])
})
it('rejects missing, nonexistent, and directory executable paths before PowerShell', () => {
expect(() => validateExecutablePath('')).toThrow(/Usage:/)
expect(() => validateExecutablePath(join(tmpdir(), 'missing-Orca.exe'))).toThrow(
/does not exist/
)
withTempFile((filePath, dir) => {
expect(() => validateExecutablePath(dir)).toThrow(/not a file/)
expect(() => validateExecutablePath(filePath)).not.toThrow()
})
})
it('parses the exact JSON emitted by PowerShell', () => {
expect(parseSignatureJson(JSON.stringify(validSignature))).toEqual(validSignature)
expect(() => parseSignatureJson('')).toThrow(/did not return/)
expect(() => parseSignatureJson(`${JSON.stringify(validSignature)}\nextra`)).toThrow(
/malformed/
)
})
it('accepts a valid signature with an exact normalized signer subject', () => {
const result = classifySignature({
...validSignature,
signerSubject: ' CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US '
})
expect(result.ok).toBe(true)
})
it('rejects invalid status and unexpected signer subjects', () => {
expect(classifySignature({ ...validSignature, status: 'NotSigned' }).message).toMatch(
/status is NotSigned/
)
expect(
classifySignature({
...validSignature,
signerSubject: 'CN=SignPath Foundation Test, O=SignPath Foundation, L=Vienna, C=AT'
}).message
).toMatch(/Unexpected Windows inner executable signer/)
})
it('accepts an expected thumbprint as an alternate explicit allowlist', () => {
expect(classifySignature({ ...validSignature, signerThumbprint: '00' }).ok).toBe(true)
expect(
classifySignature(
{ ...validSignature, signerSubject: 'CN=Rotated Signer, O=Rotated' },
{
expectedSigners: [DEFAULT_EXPECTED_SIGNER],
expectedThumbprints: ['AABBCCDD']
}
).ok
).toBe(true)
expect(
classifySignature(
{ ...validSignature, signerSubject: 'CN=Rotated Signer, O=Rotated' },
{
expectedSigners: [DEFAULT_EXPECTED_SIGNER],
expectedThumbprints: ['001122']
}
).message
).toMatch(/Unexpected Windows inner executable signer/)
expect(
classifySignature(validSignature, {
expectedSigners: [DEFAULT_EXPECTED_SIGNER],
expectedThumbprints: ['001122']
}).ok
).toBe(true)
})
it('runs PowerShell with an argument array and fails on stderr or nonzero exit', () => {
const calls = []
const spawnSyncImpl = (command, args, options) => {
calls.push({ command, args, options })
return { status: 0, stdout: JSON.stringify(validSignature), stderr: '' }
}
expect(getPowerShellSignatureJson('C:\\Path With Spaces\\Orca.exe', spawnSyncImpl)).toBe(
JSON.stringify(validSignature)
)
expect(calls[0].command).toBe('pwsh')
expect(calls[0].args).toContain('-Command')
expect(calls[0].args.at(-1)).toBe('C:\\Path With Spaces\\Orca.exe')
expect(calls[0].options).toEqual({ encoding: 'utf8' })
expect(() =>
getPowerShellSignatureJson('Orca.exe', () => ({ status: 0, stdout: '{}', stderr: 'warning' }))
).toThrow(/stderr/)
expect(() =>
getPowerShellSignatureJson('Orca.exe', () => ({ status: 7, stdout: '', stderr: '' }))
).toThrow(/exit code 7/)
})
it('verifies with injected Windows platform and spawn implementation', () => {
withTempFile((filePath) => {
const signature = verifyWindowsInnerSignature({
executablePath: filePath,
platform: 'win32',
spawnSyncImpl: () => ({ status: 0, stdout: JSON.stringify(validSignature), stderr: '' })
})
expect(signature).toEqual(validSignature)
})
})
it('does not attempt real Authenticode verification outside Windows', () => {
withTempFile((filePath) => {
expect(() =>
verifyWindowsInnerSignature({
executablePath: filePath,
platform: 'linux',
spawnSyncImpl: () => {
throw new Error('should not spawn')
}
})
).toThrow(/requires Windows/)
})
})
})