Skip to content

Commit 6e6fbb5

Browse files
committed
ci: add conditional macOS signing and notarization
1 parent 22c7594 commit 6e6fbb5

2 files changed

Lines changed: 127 additions & 17 deletions

File tree

.github/workflows/release.yml

Lines changed: 118 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ jobs:
8686
retention-days: 14
8787

8888
macos:
89-
name: macOS arm64 unsigned package
89+
name: macOS arm64 package
9090
runs-on: macos-14
9191

9292
steps:
@@ -126,6 +126,114 @@ jobs:
126126
chmod +x scripts/macos/verify-release.sh scripts/macos/verify-signed-app.sh
127127
./scripts/macos/verify-release.sh
128128
129+
- name: Determine macOS signing mode
130+
id: signing
131+
shell: bash
132+
env:
133+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
134+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
135+
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
136+
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
137+
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
138+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
139+
run: |
140+
set -euo pipefail
141+
if [[ -n "${APPLE_CERTIFICATE}" &&
142+
-n "${APPLE_CERTIFICATE_PASSWORD}" &&
143+
-n "${APPLE_API_KEY}" &&
144+
-n "${APPLE_API_KEY_ID}" &&
145+
-n "${APPLE_ISSUER_ID}" &&
146+
-n "${APPLE_TEAM_ID}" ]]; then
147+
echo "mode=signed" >> "${GITHUB_OUTPUT}"
148+
echo "PACKAGE_SUFFIX=signed" >> "${GITHUB_ENV}"
149+
else
150+
echo "mode=unsigned" >> "${GITHUB_OUTPUT}"
151+
echo "PACKAGE_SUFFIX=unsigned" >> "${GITHUB_ENV}"
152+
echo "Apple signing/notarization secrets are incomplete; keeping unsigned package mode."
153+
fi
154+
155+
- name: Import Developer ID certificate
156+
if: steps.signing.outputs.mode == 'signed'
157+
shell: bash
158+
env:
159+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
160+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
161+
run: |
162+
set -euo pipefail
163+
keychain_path="${RUNNER_TEMP}/vpnrouter-signing.keychain-db"
164+
keychain_password="$(openssl rand -hex 24)"
165+
certificate_path="${RUNNER_TEMP}/vpnrouter-certificate.p12"
166+
printf '%s' "${APPLE_CERTIFICATE}" | base64 -D > "${certificate_path}"
167+
security create-keychain -p "${keychain_password}" "${keychain_path}"
168+
security set-keychain-settings -lut 21600 "${keychain_path}"
169+
security unlock-keychain -p "${keychain_password}" "${keychain_path}"
170+
security import "${certificate_path}" \
171+
-P "${APPLE_CERTIFICATE_PASSWORD}" \
172+
-k "${keychain_path}" \
173+
-T /usr/bin/codesign \
174+
-T /usr/bin/security
175+
security list-keychains -d user -s "${keychain_path}"
176+
security default-keychain -s "${keychain_path}"
177+
signing_identity="$(security find-identity -v -p codesigning "${keychain_path}" |
178+
sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | head -n 1)"
179+
if [[ -z "${signing_identity}" ]]; then
180+
echo "Developer ID Application identity was not found in the imported certificate." >&2
181+
exit 1
182+
fi
183+
{
184+
echo "SIGNING_KEYCHAIN=${keychain_path}"
185+
echo "SIGNING_IDENTITY=${signing_identity}"
186+
} >> "${GITHUB_ENV}"
187+
188+
- name: Sign macOS app and embedded extensions
189+
if: steps.signing.outputs.mode == 'signed'
190+
shell: bash
191+
env:
192+
DERIVED_DATA_PATH: ${{ runner.temp }}/vpnrouter-derived
193+
run: |
194+
set -euo pipefail
195+
app_path="${DERIVED_DATA_PATH}/Build/Products/Release/VPNRouter.app"
196+
packet_path="${app_path}/Contents/PlugIns/PacketTunnel.appex"
197+
dns_path="${app_path}/Contents/Library/SystemExtensions/com.simple.VPNRouter.DNSProxyExtension.systemextension"
198+
codesign_args=(--force --options runtime --timestamp --keychain "${SIGNING_KEYCHAIN}" --sign "${SIGNING_IDENTITY}")
199+
codesign "${codesign_args[@]}" \
200+
--entitlements macos/VPNRouter/PacketTunnel/PacketTunnel.entitlements \
201+
"${packet_path}"
202+
codesign "${codesign_args[@]}" \
203+
--entitlements macos/VPNRouter/DNSProxyExtension/DNSProxyExtension.entitlements \
204+
"${dns_path}"
205+
codesign "${codesign_args[@]}" \
206+
--entitlements macos/VPNRouter/VPNRouter/VPNRouter.entitlements \
207+
"${app_path}"
208+
VPNROUTER_PRODUCT_VERSION="${VERSION}" \
209+
VPNROUTER_BUILD_NUMBER="${GITHUB_RUN_NUMBER}" \
210+
VPNROUTER_MINIMUM_MACOS_VERSION="15.0" \
211+
scripts/macos/verify-signed-app.sh --mode distribution "${app_path}"
212+
213+
- name: Notarize and staple macOS app
214+
if: steps.signing.outputs.mode == 'signed'
215+
shell: bash
216+
env:
217+
DERIVED_DATA_PATH: ${{ runner.temp }}/vpnrouter-derived
218+
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
219+
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
220+
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
221+
run: |
222+
set -euo pipefail
223+
app_path="${DERIVED_DATA_PATH}/Build/Products/Release/VPNRouter.app"
224+
submit_zip="${RUNNER_TEMP}/VPNRouter-${VERSION}-notarization.zip"
225+
api_key_path="${RUNNER_TEMP}/AuthKey_${APPLE_API_KEY_ID}.p8"
226+
printf '%s' "${APPLE_API_KEY}" | base64 -D > "${api_key_path}"
227+
ditto -c -k --sequesterRsrc --keepParent "${app_path}" "${submit_zip}"
228+
xcrun notarytool submit "${submit_zip}" \
229+
--key "${api_key_path}" \
230+
--key-id "${APPLE_API_KEY_ID}" \
231+
--issuer "${APPLE_ISSUER_ID}" \
232+
--wait
233+
xcrun stapler staple "${app_path}"
234+
xcrun stapler validate "${app_path}"
235+
spctl --assess --type execute "${app_path}"
236+
129237
- name: Package macOS app as ZIP and DMG
130238
shell: bash
131239
env:
@@ -137,8 +245,8 @@ jobs:
137245
mkdir -p "${output_dir}"
138246
test -d "${app_path}"
139247
140-
zip_path="${output_dir}/VPNRouter-${VERSION}-macos-arm64-unsigned.zip"
141-
dmg_path="${output_dir}/VPNRouter-${VERSION}-macos-arm64-unsigned.dmg"
248+
zip_path="${output_dir}/VPNRouter-${VERSION}-macos-arm64-${PACKAGE_SUFFIX}.zip"
249+
dmg_path="${output_dir}/VPNRouter-${VERSION}-macos-arm64-${PACKAGE_SUFFIX}.dmg"
142250
ditto -c -k --sequesterRsrc --keepParent "${app_path}" "${zip_path}"
143251
hdiutil create \
144252
-volname "VPN Router ${VERSION}" \
@@ -151,11 +259,13 @@ jobs:
151259
shasum -a 256 "${dmg_path}" > "${dmg_path}.sha256"
152260
shasum -a 256 -c "${zip_path}.sha256"
153261
shasum -a 256 -c "${dmg_path}.sha256"
154-
printf '%s\n' \
155-
"VPN Router ${VERSION} macOS arm64 package" \
156-
"This package is unsigned compile/package evidence." \
157-
"System Extension activation requires an owner-signed app installed in /Applications." \
158-
> "${output_dir}/VPNRouter-${VERSION}-macos-arm64-unsigned.txt"
262+
if [[ "${PACKAGE_SUFFIX}" == "unsigned" ]]; then
263+
printf '%s\n' \
264+
"VPN Router ${VERSION} macOS arm64 package" \
265+
"This package is unsigned compile/package evidence." \
266+
"System Extension activation requires an owner-signed app installed in /Applications." \
267+
> "${output_dir}/VPNRouter-${VERSION}-macos-arm64-unsigned.txt"
268+
fi
159269
160270
- name: Upload macOS release assets
161271
uses: actions/upload-artifact@v4

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ VPN Router는 다음 원칙을 중심으로 동작합니다.
3131
| 플랫폼 || 네트워크 계층 | 배포 상태 |
3232
|---|---|---|---|
3333
| Windows 11 x64 | WinUI 3 / .NET 10 | Windows 서비스, WireGuard, DNS, 경로, IPC | portable EXE 검증 및 GitHub Actions 준비 |
34-
| macOS 15+ Apple Silicon | SwiftUI | Packet Tunnel + DNS Proxy System Extension, Keychain | unsigned 패키지 검증 완료; 서명·공증·실제 연결 검증 진행 중 |
34+
| macOS 15+ Apple Silicon | SwiftUI | Packet Tunnel + DNS Proxy System Extension, Keychain | Actions에서 Secret 완비 시 서명·공증 패키지, 미완비 시 unsigned 검증 패키지 |
3535

3636
두 플랫폼은 홈, VPN 프로필, VPN 사이트, 문제 해결, 설정의 다섯 영역과
3737
사용자에게 보이는 작업 의미를 공유합니다. 저수준 VPN·DNS·경로·권한·저장소
@@ -96,14 +96,14 @@ Xcode에서 실행한 앱은 System Extension 활성화를 위해 `/Applications
9696
2. GitHub Actions에서 수동 실행하고 버전을 입력합니다.
9797

9898
Windows 작업은 두 솔루션과 focused test를 실행한 뒤 portable EXE와 SHA-256을
99-
만듭니다. macOS 작업은 Apple Silicon arm64 앱과 내장 확장을 검증한 뒤 unsigned
100-
ZIP, DMG와 체크섬을 만듭니다. 두 작업이 성공하면 태그 실행은 GitHub Release를
101-
자동으로 만들고 산출물을 올립니다.
102-
103-
현재 macOS Actions 산출물은 unsigned compile/package evidence입니다. 실제
104-
System Extension 배포에는 Developer ID 서명, 공증, stapling, Gatekeeper와
105-
실제 Mac 설치 검증이 추가로 필요합니다. GitHub Actions에 인증서나 개인 키를
106-
커밋하지 말고, 서명 단계를 추가할 때는 GitHub Secrets와 최소 권한을 사용합니다.
99+
만듭니다. macOS 작업은 Apple Silicon arm64 앱과 내장 확장을 검증합니다. 다음
100+
Secret이 모두 있으면 Developer ID 서명, 공증, stapling을 거쳐 signed ZIP·DMG를
101+
만들고, 하나라도 없으면 명확한 unsigned ZIP·DMG로 자동 대체합니다.
102+
103+
`APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_API_KEY`,
104+
`APPLE_API_KEY_ID`, `APPLE_ISSUER_ID`, `APPLE_TEAM_ID`는 GitHub Actions Secret으로
105+
등록합니다. 인증서나 개인 키를 저장소에 커밋하지 말고, 최종 배포 전에는 실제 Mac의
106+
`/Applications` 설치와 Gatekeeper 및 System Extension 동작도 확인해야 합니다.
107107

108108
## 구조
109109

0 commit comments

Comments
 (0)