05. macOS Notarization — Gatekeeper를 통과하기
macOS Catalina(10.15, 2019) 이후 서명만으론 부족하다. Apple 서버에 binary를 직접 업로드해 malware 스캔을 받고, 통과 ticket을 받아, binary에 박아야 사용자가 처음 열 때 OS가 통과시킨다. 이 문서는 그 4단계와 흔한 실패 사유를 짚는다.
한 줄 답
Notarization = Apple이 내 서명 위에 한 번 더 도장을 찍는 절차.
서명 → 압축 → 업로드 → 스캔 → ticket 발급 → stapler로 binary에 박기. 안 하면 macOS 10.15+에서 “Apple이 검증할 수 없는 앱” 으로 차단.
Why — 서명이 있는데 왜 또?
| 단계 | 답하는 질문 |
|---|---|
| Code Signing | ”이 binary가 누구의 것이냐” |
| Notarization | ”Apple이 그 binary를 한 번 훑어봤느냐 (malware 스캔)“ |
| Gatekeeper | ”OS가 처음 열 때 위 둘을 동시에 확인” |
Apple의 모델은 서명자의 ID는 CA가 보장하고, binary의 안전성은 Apple이 직접 확인한다는 이중 게이트다. 그래서 Developer ID 인증서를 훔쳐서 malware를 서명해도, Apple 서버가 스캔에서 잡으면 notarization 실패 → 배포 불가.
How — 4단계 파이프라인
Step 1: hardened runtime + entitlements로 서명
이미 04 code signing에서 다뤘다. notarization을 위해서는 반드시:
--options runtime(hardened runtime)- 적절한 entitlements (
allow-jit,allow-unsigned-executable-memory등 Electron 필수) - 모든 내부 binary에 재귀 서명 (
--deep또는 electron-builder 자동)
Step 2: zip으로 압축
# .app을 zip으로 — ditto가 추천 (metadata 보존)
ditto -c -k --keepParent MyApp.app MyApp.zip
dmg나pkg로도 notarize 가능하지만, 내부 .app도 따로 notarize해야 안전. electron-builder는 자동으로 처리.
Step 3: Apple notarytool로 제출
# 2021년 이후 권장 도구 (구 altool은 2023년 deprecate)
xcrun notarytool submit MyApp.zip \
--apple-id "ci@mycompany.com" \
--team-id "XXXXXXXXXX" \
--password "abcd-efgh-ijkl-mnop" \ # app-specific password
--wait
# 또는 keychain 프로필 사용 (더 안전)
xcrun notarytool store-credentials "my-profile" \
--apple-id "..." --team-id "..." --password "..."
xcrun notarytool submit MyApp.zip --keychain-profile my-profile --wait--wait: 결과 나올 때까지 대기 (수 분~수십 분).--password는 Apple ID 본 비밀번호가 아니라 app-specific password (appleid.apple.com에서 생성).- 2FA가 켜진 Apple ID 필수.
notarytool은 Apple의 클라우드 큐에 binary를 올린다. 큐가 밀리면 수 시간 걸리기도 한다 (특히 출시 시즌).
Step 4: stapler로 ticket 박기
# notarize 통과한 .app에 ticket을 *물리적으로* 박는다
xcrun stapler staple MyApp.app
xcrun stapler validate MyApp.appstapler가 없으면 — 사용자가 오프라인에서 처음 열 때 Gatekeeper가 Apple 서버에 못 물어봐서 차단. stapler로 ticket을 binary에 오프라인 검증 가능하게 박는다.
electron-builder 자동화
mac:
hardenedRuntime: true
entitlements: build/entitlements.mac.plist
notarize: true # 또는 아래 객체 형태
# 또는
# notarize:
# teamId: "XXXXXXXXXX"환경변수로 credentials 전달:
export APPLE_ID="ci@mycompany.com"
export APPLE_APP_SPECIFIC_PASSWORD="abcd-efgh-ijkl-mnop"
export APPLE_TEAM_ID="XXXXXXXXXX"
npm run distelectron-builder는 zip 생성 → notarytool submit → stapler까지 자동.
What — entitlements 안의 핵심 키 (Electron용)
| key | 의미 | Electron에 필요한가 |
|---|---|---|
com.apple.security.cs.allow-jit | V8 JIT 컴파일 | 필수 (없으면 V8 segfault) |
com.apple.security.cs.allow-unsigned-executable-memory | 동적 코드 실행 영역 | 필수 (Electron native bindings) |
com.apple.security.cs.allow-dyld-environment-variables | DYLD_* 환경변수 | 빌드 도구가 필요할 수 있음 |
com.apple.security.cs.disable-library-validation | 서명 안 된 dylib 로드 | 일부 네이티브 모듈에 필요 |
com.apple.security.cs.disable-executable-page-protection | 페이지 보호 해제 | 거의 안 씀 (위험) |
com.apple.security.device.camera | 카메라 접근 | 필요 시만 |
com.apple.security.device.audio-input | 마이크 접근 | 필요 시만 |
com.apple.security.network.client | 아웃바운드 네트워크 | sandbox 빌드 시 필수 |
com.apple.security.files.user-selected.read-write | 사용자가 명시 선택한 파일 RW | sandbox 빌드 시 |
최소 권한 원칙 — 필요한 것만. 과한 entitlements는 Apple 리뷰가 까다로워질 수 있고, 사용자의 신뢰 신호가 떨어진다.
What-if — 흔한 실패 사례
Apple notarytool은 실패 사유를 log url로 돌려준다. 받는 즉시:
xcrun notarytool log <submission-id> --keychain-profile my-profile사례 1: “The binary uses an SDK older than the 10.9 SDK”
The binary at path [...] uses an SDK older than the 10.9 SDK.- 원인: native module이 너무 오래된 macOS SDK로 빌드됨.
- 해결: native module rebuild (
npm rebuild) — 빌드 머신의 Xcode가 최신인지 확인. Electron의electron-rebuild.
사례 2: “The signature does not include a secure timestamp”
The signature of the binary is invalid: ...
The signature does not include a secure timestamp.- 원인:
codesign시--timestamp누락. - 해결: electron-builder는 자동. 직접 codesign 호출 시
--timestamp옵션 명시.
사례 3: “The signature algorithm used is too weak”
The signature algorithm used is too weak.- 원인: SHA-1로 서명 (이미 deprecate).
- 해결: SHA-256 강제 —
signingHashAlgorithms: [sha256].
사례 4: “The executable does not have the hardened runtime enabled”
The executable does not have the hardened runtime enabled.- 원인: 내부 binary 중 하나에
--options runtime이 안 붙음. - 해결: electron-builder의
hardenedRuntime: true. 직접 codesign이면 모든 내부 binary에 동일 옵션.
사례 5: “The binary is not signed”
The binary at path [...] is not signed at all.- 원인: native module의 sub-binary가 서명 안 됨. electron-builder가 인식 못 하는 위치에 binary가 있을 때 흔함.
- 해결:
afterSignhook으로 누락된 binary 수동 서명, 또는signIgnore로 서명 대상에서 제외 (대신 .app 본체 서명에 영향 없도록 위치 조정).
사례 6: “Team ID does not match”
The team ID in the request does not match the team of the signing certificate.- 원인: 여러 Apple Developer 계정을 갖고 있는 organization에서 다른 team의 인증서로 서명.
- 해결:
--team-id와 인증서의 team ID 일치 확인.security find-identity+codesign -dv로 검증.
사례 7: notarize는 통과했는데 사용자에게는 “검증 안 됨”
- 원인: stapler를 깜빡함. ticket이 binary에 박히지 않음 → 오프라인 검증 실패.
- 해결:
xcrun stapler staple .app+dmg에는 내부 .app이 staple된 채로 들어가야 함. electron-builder는 자동.
사례 8: dmg 자체는 staple됐는데 내부 .app은 안 됨
- 원인: dmg에 ticket을 박았지만 .app은 dmg 외부에서 staple되지 않음. 사용자가 dmg에서 .app을 끌어다 놓는 순간, OS는 .app만 본다.
- 해결:
.app자체도 staple하고, 그 staple된 .app을 dmg에 넣음. (electron-builder는 이 순서대로 자동 처리.)
What — 검증 방법
# 1. 서명 검증
codesign --verify --deep --strict --verbose=2 MyApp.app
# 2. Gatekeeper 시뮬레이션 (이게 가장 중요)
spctl -a -t exec -vv MyApp.app
# 통과: "MyApp.app: accepted, source=Notarized Developer ID"
# 3. notarization ticket 확인
xcrun stapler validate MyApp.app
# 통과: "The validate action worked!"
# 4. quarantine 비트 확인 (다운로드된 파일에만 있음)
xattr -p com.apple.quarantine MyApp.app
# 결과 있음: 사용자가 받았을 때의 상태와 동일
# 5. 실제 사용자 시뮬레이션 — dmg를 받아서 끌어다 놓은 상태
ditto MyApp.app /tmp/test-MyApp.app
xattr -w com.apple.quarantine "0083;00000000;Safari;" /tmp/test-MyApp.app
open /tmp/test-MyApp.appInsight — 흥미로운 이야기
“Catalina(10.15, 2019)의 조용한 강행 — 그리고 폭발한 이슈 트래커들”
Apple은 macOS 10.14.5(2019년 6월)부터 새로 서명된 모든 binary는 notarize되어야 한다는 정책을 조용히 도입했다. 그리고 10.15에서 강제했다. 그 결과 — 2019년 가을 모든 Electron OSS 프로젝트의 이슈 트래커가 “Catalina에서 안 열림” 글로 도배됐다. electron-builder, electron-osx-sign 같은 도구가 동시다발적으로 notarize 통합을 급히 추가했다. 그 시기에 signing/notarize CI 파이프라인을 처음 갖춘 팀이 많다.
“altool은 죽었다. notarytool 만세.”
2018~2022년에는
xcrun altool --notarize-app이 표준이었다. 느렸다 (제출 자체에 분 단위). 2021년 Apple은notarytool을 출시했고 — 10배 이상 빠르다 (제출 자체는 초 단위, 큐 대기만 분 단위). 2023년 altool은 완전 deprecated — 아직도 옛 가이드에altool명령이 보이면 오래된 문서임을 알 수 있다.
“Apple 큐는 출시 시즌에 폭주한다”
WWDC 직후, 새 Xcode 출시 직후, 연말 OS 메이저 업데이트 직후 — 전 세계 개발자가 동시에 notarize 큐에 작업을 던진다. 평소 5분이 2시간까지 늘어난다. 그래서 큰 출시를 앞두고 있다면 — 마지막 빌드를 출시일 전날에 미리 돌려두는 게 운영 노하우다.
요약 + Mermaid
Notarization은 서명 위에 Apple이 한 번 더 도장 찍는 절차다.
서명 → 압축 → notarytool submit → stapler staple4단계. hardened runtime + 올바른 entitlements가 전제조건이고, Electron 특유의 키 (allow-jit,allow-unsigned-executable-memory)가 필수. 실패 사유는 log url에서 정확히 나오므로, 막혔을 때 가장 먼저 그걸 본다.