Skip to content

Commit 6a63d40

Browse files
authored
[codex] Guard desktop release signing (#25)
* fix(desktop): remove duplicate app version setting * fix: guard desktop release signing
1 parent b12027f commit 6a63d40

5 files changed

Lines changed: 312 additions & 27 deletions

File tree

.github/workflows/desktop-release.yml

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ on:
4646
required: true
4747
default: false
4848
type: boolean
49+
allow_unsigned_test_release:
50+
description: "Allow unsigned macOS/Windows artifacts only for prerelease testing."
51+
required: true
52+
default: false
53+
type: boolean
4954
clobber:
5055
description: "Replace same-named assets when uploading to an existing release."
5156
required: true
@@ -281,11 +286,18 @@ jobs:
281286
- name: Configure optional signing secrets
282287
shell: bash
283288
env:
289+
IS_DRY_RUN: ${{ inputs.dry_run }}
284290
APPLE_APP_SPECIFIC_PASSWORD_SECRET: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
285291
APPLE_ID_SECRET: ${{ secrets.APPLE_ID }}
286292
APPLE_TEAM_ID_SECRET: ${{ secrets.APPLE_TEAM_ID }}
293+
ALLOW_UNSIGNED_TEST_RELEASE: ${{ inputs.allow_unsigned_test_release }}
294+
IS_PRERELEASE: ${{ inputs.prerelease }}
295+
MAC_CSC_KEY_PASSWORD_SECRET: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
296+
MAC_CSC_LINK_SECRET: ${{ secrets.MAC_CSC_LINK }}
287297
CSC_KEY_PASSWORD_SECRET: ${{ secrets.CSC_KEY_PASSWORD }}
288298
CSC_LINK_SECRET: ${{ secrets.CSC_LINK }}
299+
WIN_CSC_KEY_PASSWORD_SECRET: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
300+
WIN_CSC_LINK_SECRET: ${{ secrets.WIN_CSC_LINK }}
289301
SENTRY_ELECTRON_INGEST_URL_SECRET: ${{ secrets.SENTRY_ELECTRON_INGEST_URL }}
290302
run: |
291303
set -euo pipefail
@@ -305,15 +317,71 @@ jobs:
305317
} >> "$GITHUB_ENV"
306318
}
307319
308-
if [ -n "$CSC_LINK_SECRET" ]; then
309-
append_env "CSC_LINK" "$CSC_LINK_SECRET"
310-
append_env "CSC_KEY_PASSWORD" "$CSC_KEY_PASSWORD_SECRET"
311-
append_env "APPLE_ID" "$APPLE_ID_SECRET"
312-
append_env "APPLE_APP_SPECIFIC_PASSWORD" "$APPLE_APP_SPECIFIC_PASSWORD_SECRET"
313-
append_env "APPLE_TEAM_ID" "$APPLE_TEAM_ID_SECRET"
314-
echo "CSC_IDENTITY_AUTO_DISCOVERY=true" >> "$GITHUB_ENV"
320+
mac_csc_link="${MAC_CSC_LINK_SECRET:-$CSC_LINK_SECRET}"
321+
mac_csc_key_password="${MAC_CSC_KEY_PASSWORD_SECRET:-$CSC_KEY_PASSWORD_SECRET}"
322+
323+
allow_unsigned_artifacts() {
324+
if [ "$IS_DRY_RUN" = "true" ]; then
325+
return 0
326+
fi
327+
328+
if [ "$ALLOW_UNSIGNED_TEST_RELEASE" = "true" ] && [ "$IS_PRERELEASE" = "true" ]; then
329+
return 0
330+
fi
331+
332+
return 1
333+
}
334+
335+
if [ "$RUNNER_OS" = "macOS" ]; then
336+
if [ -n "$mac_csc_link" ]; then
337+
if [ -z "$mac_csc_key_password" ]; then
338+
echo "::error::MAC_CSC_LINK/CSC_LINK is configured, but MAC_CSC_KEY_PASSWORD/CSC_KEY_PASSWORD is missing."
339+
exit 1
340+
fi
341+
342+
append_env "CSC_LINK" "$mac_csc_link"
343+
append_env "CSC_KEY_PASSWORD" "$mac_csc_key_password"
344+
append_env "APPLE_ID" "$APPLE_ID_SECRET"
345+
append_env "APPLE_APP_SPECIFIC_PASSWORD" "$APPLE_APP_SPECIFIC_PASSWORD_SECRET"
346+
append_env "APPLE_TEAM_ID" "$APPLE_TEAM_ID_SECRET"
347+
echo "CSC_IDENTITY_AUTO_DISCOVERY=true" >> "$GITHUB_ENV"
348+
else
349+
if ! allow_unsigned_artifacts; then
350+
echo "::error::Published macOS desktop releases require MAC_CSC_LINK/CSC_LINK and MAC_CSC_KEY_PASSWORD/CSC_KEY_PASSWORD so auto-update signature validation can pass."
351+
exit 1
352+
fi
353+
354+
if [ "$IS_DRY_RUN" = "false" ]; then
355+
echo "::warning::Publishing an unsigned macOS prerelease for manual testing. Auto-update validation is not supported for this artifact."
356+
fi
357+
358+
echo "CSC_IDENTITY_AUTO_DISCOVERY=false" >> "$GITHUB_ENV"
359+
fi
360+
elif [ "$RUNNER_OS" = "Windows" ]; then
361+
if [ -n "$WIN_CSC_LINK_SECRET" ]; then
362+
if [ -z "$WIN_CSC_KEY_PASSWORD_SECRET" ]; then
363+
echo "::error::WIN_CSC_LINK is configured, but WIN_CSC_KEY_PASSWORD is missing."
364+
exit 1
365+
fi
366+
367+
append_env "WIN_CSC_LINK" "$WIN_CSC_LINK_SECRET"
368+
append_env "WIN_CSC_KEY_PASSWORD" "$WIN_CSC_KEY_PASSWORD_SECRET"
369+
else
370+
if ! allow_unsigned_artifacts; then
371+
echo "::error::Published Windows desktop releases require WIN_CSC_LINK and WIN_CSC_KEY_PASSWORD."
372+
exit 1
373+
fi
374+
375+
if [ "$IS_DRY_RUN" = "false" ]; then
376+
echo "::warning::Publishing an unsigned Windows prerelease for manual testing. Windows may show Unknown Publisher / SmartScreen warnings."
377+
else
378+
echo "Windows signing certificate is not configured; Windows dry-run artifacts will be unsigned."
379+
fi
380+
fi
315381
else
316-
echo "CSC_IDENTITY_AUTO_DISCOVERY=false" >> "$GITHUB_ENV"
382+
if [ "$RUNNER_OS" != "Linux" ] && [ -n "$CSC_LINK_SECRET" ]; then
383+
echo "::warning::CSC_LINK is configured but not used on $RUNNER_OS."
384+
fi
317385
fi
318386
319387
append_env "SENTRY_ELECTRON_INGEST_URL" "$SENTRY_ELECTRON_INGEST_URL_SECRET"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, it } from 'bun:test'
2+
3+
import {
4+
getMacAppBundlePath,
5+
parseMacCodeSignatureStatus,
6+
} from '../auto-update-signature'
7+
8+
describe('auto-update-signature', () => {
9+
it('maps an executable path back to the macOS app bundle', () => {
10+
expect(getMacAppBundlePath('/Applications/OpenWork.app/Contents/MacOS/OpenWork'))
11+
.toBe('/Applications/OpenWork.app')
12+
})
13+
14+
it('rejects ad-hoc signatures because they pin updates to a cdhash', () => {
15+
const status = parseMacCodeSignatureStatus('/Applications/OpenWork.app', 0, [
16+
'Signature=adhoc',
17+
'TeamIdentifier=not set',
18+
].join('\n'))
19+
20+
expect(status.trustedForAutoUpdate).toBe(false)
21+
expect(status.reason).toBe('adhoc-signature')
22+
})
23+
24+
it('rejects unsigned apps', () => {
25+
const status = parseMacCodeSignatureStatus(
26+
'/Applications/OpenWork.app',
27+
1,
28+
'/Applications/OpenWork.app: code object is not signed at all',
29+
)
30+
31+
expect(status.trustedForAutoUpdate).toBe(false)
32+
expect(status.reason).toBe('codesign-failed')
33+
})
34+
35+
it('accepts signed apps with a TeamIdentifier', () => {
36+
const status = parseMacCodeSignatureStatus('/Applications/OpenWork.app', 0, [
37+
'Authority=Developer ID Application: Example Inc (ABCDE12345)',
38+
'TeamIdentifier=ABCDE12345',
39+
].join('\n'))
40+
41+
expect(status.trustedForAutoUpdate).toBe(true)
42+
expect(status.teamIdentifier).toBe('ABCDE12345')
43+
})
44+
})
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { spawnSync } from 'child_process'
2+
import * as path from 'path'
3+
4+
export interface MacCodeSignatureStatus {
5+
trustedForAutoUpdate: boolean
6+
appBundlePath: string
7+
reason?: 'codesign-failed' | 'adhoc-signature' | 'missing-team-identifier'
8+
signature?: string
9+
teamIdentifier?: string
10+
diagnostic?: string
11+
}
12+
13+
function parseCodesignField(output: string, field: string): string | undefined {
14+
const match = output.match(new RegExp(`^${field}=(.*)$`, 'm'))
15+
return match?.[1]?.trim()
16+
}
17+
18+
export function getMacAppBundlePath(executablePath: string): string {
19+
return path.dirname(path.dirname(path.dirname(executablePath)))
20+
}
21+
22+
export function parseMacCodeSignatureStatus(
23+
appBundlePath: string,
24+
exitStatus: number | null,
25+
output: string,
26+
): MacCodeSignatureStatus {
27+
if (exitStatus !== 0) {
28+
return {
29+
trustedForAutoUpdate: false,
30+
appBundlePath,
31+
reason: 'codesign-failed',
32+
diagnostic: output.trim(),
33+
}
34+
}
35+
36+
const signature = parseCodesignField(output, 'Signature')
37+
const teamIdentifier = parseCodesignField(output, 'TeamIdentifier')
38+
39+
if (signature === 'adhoc') {
40+
return {
41+
trustedForAutoUpdate: false,
42+
appBundlePath,
43+
reason: 'adhoc-signature',
44+
signature,
45+
teamIdentifier,
46+
}
47+
}
48+
49+
if (!teamIdentifier || teamIdentifier === 'not set') {
50+
return {
51+
trustedForAutoUpdate: false,
52+
appBundlePath,
53+
reason: 'missing-team-identifier',
54+
signature,
55+
teamIdentifier,
56+
}
57+
}
58+
59+
return {
60+
trustedForAutoUpdate: true,
61+
appBundlePath,
62+
signature,
63+
teamIdentifier,
64+
}
65+
}
66+
67+
export function getCurrentMacCodeSignatureStatus(executablePath: string): MacCodeSignatureStatus {
68+
const appBundlePath = getMacAppBundlePath(executablePath)
69+
const result = spawnSync('/usr/bin/codesign', ['-d', '-vvv', appBundlePath], {
70+
encoding: 'utf8',
71+
})
72+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`
73+
74+
return parseMacCodeSignatureStatus(appBundlePath, result.status, output)
75+
}

0 commit comments

Comments
 (0)