forked from fleetdm/fleet
-
Notifications
You must be signed in to change notification settings - Fork 0
253 lines (220 loc) · 11.4 KB
/
Copy pathfleet-desktop-macos-build.yml
File metadata and controls
253 lines (220 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
name: Build Fleet Desktop (macOS)
# Builds the native macOS Fleet Desktop app (apps/fleet-desktop-macos/) and its
# embedded Platform SSO extension (FleetPSSOExtension.appex), code signs and
# notarizes them with Fleet's Developer ID certificates, and uploads the signed
# .pkg as a workflow artifact. No GitHub Release is created. Also callable as a
# reusable workflow (workflow_call), which is how release-fleet-desktop-macos.yml
# produces the pkg it publishes to download.fleetdm.com.
#
# The app and extension carry managed Associated Domains entitlements
# (com.apple.developer.associated-domains{,.mdm-managed}). Those are restricted
# entitlements: codesign only honors them when a Developer ID provisioning
# profile that grants them is embedded in the bundle. The profiles are provided
# as base64 repo secrets (never committed) and embedded at sign time — see the
# README's "Signing secrets" section.
#
# This workflow always signs and notarizes. If the certs or provisioning
# profiles are unavailable (e.g. a fork PR that can't read secrets), it fails
# loudly rather than producing an unsigned artifact.
on:
push:
branches:
- main
paths:
- 'apps/fleet-desktop-macos/**'
- '.github/workflows/fleet-desktop-macos-build.yml'
pull_request:
paths:
- 'apps/fleet-desktop-macos/**'
- '.github/workflows/fleet-desktop-macos-build.yml'
workflow_dispatch:
workflow_call:
secrets:
APPLE_APPLICATION_CERTIFICATE:
required: true
APPLE_APPLICATION_CERTIFICATE_PASSWORD:
required: true
APPLE_INSTALLER_CERTIFICATE:
required: true
APPLE_INSTALLER_CERTIFICATE_PASSWORD:
required: true
KEYCHAIN_PASSWORD:
required: true
APPLE_FLEET_DESKTOP_APP_PROFILE_B64:
required: true
APPLE_PSSO_EXT_PROFILE_B64:
required: true
APPLE_USERNAME:
required: true
APPLE_PASSWORD:
required: true
APPLE_TEAM_ID:
required: true
# Cancel superseded runs on the same ref.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
defaults:
run:
# fail-fast using bash -eo pipefail.
shell: bash
working-directory: apps/fleet-desktop-macos
permissions:
contents: read
env:
# Fleet's Developer ID certificate identities (SHA-1). Same team as the rest
# of Fleet's macOS artifacts (orbit Fleet Desktop, fleetd-base.pkg).
APPLICATION_SIGNING_IDENTITY_SHA1: 604D877399AAEB7630A78B84F288E2D28A2EDE42
INSTALLER_SIGNING_IDENTITY_SHA1: 4608F71FB42E1845C7FC9B2D2B6A7A8D11BBD940
# Embedded SSO extension bundle (relative to Fleet Desktop.app/Contents).
APPEX_REL_PATH: PlugIns/FleetPSSOExtension.appex
jobs:
build:
name: Build, sign, and notarize Fleet Desktop (macOS)
runs-on: macos-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
persist-credentials: false
- name: Build app (with embedded extension) and create pkg
run: |
chmod +x build.sh build-pkg.sh
./build-pkg.sh
- name: Import Developer ID certificates
env:
APPLE_APPLICATION_CERTIFICATE: ${{ secrets.APPLE_APPLICATION_CERTIFICATE }}
APPLE_APPLICATION_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APPLICATION_CERTIFICATE_PASSWORD }}
APPLE_INSTALLER_CERTIFICATE: ${{ secrets.APPLE_INSTALLER_CERTIFICATE }}
APPLE_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
# Developer ID Application certificate — signs the .app/.appex (codesign).
echo "$APPLE_APPLICATION_CERTIFICATE" | base64 --decode > application.p12
security import application.p12 -k build.keychain -P "$APPLE_APPLICATION_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
rm application.p12
# Developer ID Installer certificate — signs the .pkg (productsign).
echo "$APPLE_INSTALLER_CERTIFICATE" | base64 --decode > installer.p12
security import installer.p12 -k build.keychain -P "$APPLE_INSTALLER_CERTIFICATE_PASSWORD" -T /usr/bin/productsign
rm installer.p12
security set-key-partition-list -S apple-tool:,apple:,codesign:,productsign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security find-identity -vv
- name: Embed provisioning profiles
env:
APPLE_FLEET_DESKTOP_APP_PROFILE_B64: ${{ secrets.APPLE_FLEET_DESKTOP_APP_PROFILE_B64 }}
APPLE_PSSO_EXT_PROFILE_B64: ${{ secrets.APPLE_PSSO_EXT_PROFILE_B64 }}
run: |
APP="build/Fleet Desktop.app"
APPEX="$APP/Contents/$APPEX_REL_PATH"
if [ -z "$APPLE_FLEET_DESKTOP_APP_PROFILE_B64" ] || [ -z "$APPLE_PSSO_EXT_PROFILE_B64" ]; then
echo "::error::Missing provisioning profile secrets (APPLE_FLEET_DESKTOP_APP_PROFILE_B64 / APPLE_PSSO_EXT_PROFILE_B64). The app and extension carry restricted Associated Domains entitlements that codesign cannot honor without them."
exit 1
fi
# Developer ID profiles authorizing the restricted entitlements.
echo "$APPLE_PSSO_EXT_PROFILE_B64" | base64 --decode > "$APPEX/Contents/embedded.provisionprofile"
echo "$APPLE_FLEET_DESKTOP_APP_PROFILE_B64" | base64 --decode > "$APP/Contents/embedded.provisionprofile"
- name: Verify profiles authorize the signing certificate
run: |
APP="build/Fleet Desktop.app"
APPEX="$APP/Contents/$APPEX_REL_PATH"
# AMFI requires the signing certificate to be listed in the embedded
# profile's DeveloperCertificates, or it SIGKILLs the app at launch.
# codesign, Gatekeeper, and notarization all pass regardless — so
# without this check a profile cut against the wrong cert produces a
# signed, notarized pkg that silently won't launch. Even two certs from
# the same team will result in a broken, unusable app - they must be the
# same cert
check='import sys,plistlib,hashlib; pl=plistlib.loads(sys.stdin.buffer.read()); h=[hashlib.sha1(bytes(c)).hexdigest().upper() for c in pl.get("DeveloperCertificates",[])]; print(" authorizes:",h); sys.exit(0 if sys.argv[1].upper() in h else 1)'
for prof in "$APPEX/Contents/embedded.provisionprofile" "$APP/Contents/embedded.provisionprofile"; do
echo "Checking $prof"
if ! security cms -D -i "$prof" | python3 -c "$check" "$APPLICATION_SIGNING_IDENTITY_SHA1"; then
echo "::error::$prof does not authorize signing certificate $APPLICATION_SIGNING_IDENTITY_SHA1. AMFI will SIGKILL the app at launch (notarization does NOT catch this). Regenerate the Developer ID provisioning profile selecting that certificate."
exit 1
fi
done
- name: Code sign app and extension
run: |
APP="build/Fleet Desktop.app"
APPEX="$APP/Contents/$APPEX_REL_PATH"
# Sign inside-out: the embedded extension first, then the host app.
# Each bundle is sealed with its own entitlements + embedded profile.
codesign --force --sign "$APPLICATION_SIGNING_IDENTITY_SHA1" \
--options runtime --timestamp "$APPEX/Contents/MacOS/FleetPSSOExtension"
codesign --force --sign "$APPLICATION_SIGNING_IDENTITY_SHA1" \
--options runtime --timestamp \
--entitlements FleetPSSOExtension/FleetPSSOExtension.entitlements "$APPEX"
codesign --force --sign "$APPLICATION_SIGNING_IDENTITY_SHA1" \
--options runtime --timestamp "$APP/Contents/MacOS/FleetDesktop"
codesign --force --sign "$APPLICATION_SIGNING_IDENTITY_SHA1" \
--options runtime --timestamp \
--entitlements FleetDesktop/FleetDesktop.entitlements "$APP"
codesign --verify --deep --strict --verbose=2 "$APP"
codesign --display --verbose=4 "$APP"
codesign --display --entitlements - "$APPEX"
- name: Rebuild pkg with signed app
run: |
# build-pkg.sh reuses the already-signed app (ditto preserves the
# signature and the embedded, signed appex).
./build-pkg.sh
- name: Sign pkg
run: |
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "FleetDesktop/Info.plist")
UNSIGNED_PKG="build/dist/fleet_desktop-v${VERSION}.pkg"
SIGNED_PKG="build/dist/fleet_desktop-v${VERSION}-signed.pkg"
if [ ! -f "$UNSIGNED_PKG" ]; then
echo "Error: package not found: $UNSIGNED_PKG"
ls -la build/dist/ || true
exit 1
fi
productsign --sign "$INSTALLER_SIGNING_IDENTITY_SHA1" --timestamp \
"$UNSIGNED_PKG" "$SIGNED_PKG"
mv "$SIGNED_PKG" "$UNSIGNED_PKG"
pkgutil --check-signature "$UNSIGNED_PKG"
- name: Notarize pkg
env:
AC_USERNAME: ${{ secrets.APPLE_USERNAME }}
AC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
AC_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "FleetDesktop/Info.plist")
PKG_PATH="build/dist/fleet_desktop-v${VERSION}.pkg"
SUBMISSION_OUTPUT=$(xcrun notarytool submit "$PKG_PATH" \
--apple-id "$AC_USERNAME" \
--password "$AC_PASSWORD" \
--team-id "$AC_TEAM_ID" \
--wait --timeout 30m 2>&1) || NOTARIZATION_FAILED=true
echo "$SUBMISSION_OUTPUT"
SUBMISSION_ID=$(echo "$SUBMISSION_OUTPUT" | grep -i "id:" | head -1 | awk '{print $NF}' | tr -d ',' || echo "")
STATUS=$(echo "$SUBMISSION_OUTPUT" | grep -i "status:" | tail -1 | awk '{print $NF}' || echo "")
# Fail closed: only an explicit "Accepted" passes. notarytool statuses
# are Accepted / In Progress / Invalid / Rejected — a broad grep for
# "failed|error" would let a "Rejected" submission slip through as
# success, and notarytool can exit 0 even on a rejected package.
if [ "${NOTARIZATION_FAILED:-false}" = "true" ] || [ "$STATUS" != "Accepted" ]; then
echo "::error::Notarization failed (status: ${STATUS:-unknown})"
if [ -n "$SUBMISSION_ID" ]; then
xcrun notarytool log "$SUBMISSION_ID" \
--apple-id "$AC_USERNAME" --password "$AC_PASSWORD" --team-id "$AC_TEAM_ID" || true
fi
exit 1
fi
xcrun stapler staple "$PKG_PATH"
xcrun stapler validate "$PKG_PATH"
spctl --assess --type install --verbose "$PKG_PATH"
- name: Cleanup keychain
if: always()
run: security delete-keychain build.keychain || true
- name: Upload pkg artifact
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: fleet_desktop-pkg
path: ./apps/fleet-desktop-macos/build/dist/fleet_desktop-v*.pkg
retention-days: 30
if-no-files-found: error