"공증을 안 하면 hardened runtime으로 얻는 게 없어."
남의 Mac을 위해 만든 스위치 세 개
App Store 밖으로 Mac 앱을 내보내려면 애플이 정한 세 단계를 거쳐야 해. Developer ID Application 인증서로 서명하고, hardened runtime(codesign --options runtime)을 켜고, 공증을 받아. 공증은 앱을 애플 공증 서비스에 올려 검사받고, 낯선 사람의 Mac에서 Gatekeeper가 확인할 티켓을 받는 과정이야. Mac App Store는 여기에 네 번째로 App Sandbox를 얹어. 이 모든 건 앱을 직접 만들지 않은 사람도 그 앱을 믿을 수 있게 하려는 장치야. 가족 Mac 앱은 만든 사람이 직접 돌리고, 돌리는 Mac엔 서명 인증서가 이미 들어가 있어. 그래서 가족은 이 길로 안 가. 그리고 바로 그래서, 템플릿이 대신 켜버리기 전에 각 스위치가 정확히 뭘 하는지 알아둬야 해.
hardened runtime이 실제로 바꾸는 것
남의 말을 옮기지 말고 직접 재봐. 같은 바이너리를 자체 서명 신원으로 두 번 서명하는데, 한 번은 --options runtime을 붙여. CodeDirectory 플래그가 달라지고(0x0(none)과 0x10000(runtime)), cdhash도 달라져. 근데 designated requirement는 안 달라져. 둘 다 identifier "…" and certificate leaf = H"…"이고 글자 하나 안 틀려. 가족 문서에 이 플래그가 requirement를 바꾼다고 적힌 적이 있었어. 하지만 인증서로 서명한 앱에선 안 바뀌어. 직접 재봐야 하는 이유가 바로 이런 주장 때문이야.
진짜 비용은 hardened runtime이 켜는 library validation이야. hardened 프로세스는 애플이 서명했거나 Team ID가 같은 라이브러리만 로드할 수 있어. 자체 서명 인증서엔 Team ID가 없어(TeamIdentifier=not set). 그래서 hardened 앱은 자기 인증서로 서명한 dylib까지 "mapping process and mapped file (non-platform) have different Team IDs"라며 거부해. com.apple.security.cs.disable-library-validation 엔타이틀먼트를 넣으면 다시 로드되긴 해. 하지만 그러면 핵심 보호를 제 손으로 꺼버린 hardened runtime만 남는 거지. 이 런타임은 따로 예외 엔타이틀먼트를 넣지 않는 한 DYLD_* 환경 변수, 서명 안 된 실행 메모리, 디버거 붙이기도 막아.
결론, 그리고 그걸 보여주는 앱 둘
가족 기본값은 끔이야. 켜는 레포는 왜 켜는지 적어두고, 어느 쪽이든 빌드 관문이 상태를 확인해. 코딩 엔진의 클라이언트는 예외 엔타이틀먼트 없이 hardened로 서명해. 남의 라이브러리를 로드하지 않고, 엔진은 따로 서명된 자식 프로세스라 hardened 부모도 띄울 수 있거든. 영상 플레이어는 고정한 libmpv 의존성 묶음을 번들에 넣고 서명하니까, runtime 플래그가 있으면 doctor 스크립트가 실패해. 설정은 정반대인데 규칙은 하나야. 서명 상태는 확인하는 거지, 짐작하는 게 아니야.
공증과 샌드박스
공증엔 Developer ID 인증서, hardened runtime, 보안 타임스탬프가 필요해. 가족 로컬 스크립트는 --timestamp=none을 넘기니까, 그렇게 서명한 앱은 하나도 공증을 못 받아. App Sandbox는 모든 자식 프로세스가 물려받아. 그래서 사용자가 고른 레포에서 인터프리터를 띄우는 앱에는 못 써. 그리고 앱이 직접 고르지 않은 경로는 사용자가 접근을 허락해줘야 해서, 다른 앱이 넘겨준 경로를 여는 플레이어에도 못 써. 이 앱 중 하나가 함대 밖으로 나가는 날이 와도, 비용은 이미 치른 셈이야. Developer Program 멤버십에 Developer ID가 들어 있거든. iOS엔 가지 않은 길이란 게 없어. 모든 앱이 샌드박스 안에서 돌고, iOS 트랙은 거기서부터 시작해.