본문 바로가기
C.W.K.
Lesson 06 of 06 · published

가지 않은 길: hardened runtime, 공증, 샌드박스

~16 min · bundle-signing, hardened-runtime, notarization, app-sandbox, library-validation

Level 0번들 열어본 사람
0 XP0/84 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"공증을 안 하면 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 트랙은 거기서부터 시작해.

Code

hardened runtime 재보기: 플래그는 바뀌고, requirement는 그대로고, 자체 서명 dylib은 거부돼·bash
clang -dynamiclib lib.c -o libspark.dylib && clang main.c -o probe && cp probe probe-rt
codesign --force --sign "Spark Local Signing" libspark.dylib
codesign --force --sign "Spark Local Signing" --identifier com.example.probe probe
codesign --force --sign "Spark Local Signing" --identifier com.example.probe --options runtime probe-rt

codesign -dv probe    2>&1 | grep CodeDirectory   # flags=0x0(none)
codesign -dv probe-rt 2>&1 | grep CodeDirectory   # flags=0x10000(runtime)
diff <(codesign -d -r- probe 2>&1 | grep designated) \
     <(codesign -d -r- probe-rt 2>&1 | grep designated) && echo "same requirement"
codesign -dv probe-rt 2>&1 | grep TeamIdentifier  # TeamIdentifier=not set

# Absolute paths: a hardened program refuses a relative dlopen path before library validation runs.
./probe    "$PWD/libspark.dylib"   # loaded, answer=42
./probe-rt "$PWD/libspark.dylib"   # dlopen failed: ... have different Team IDs
앱이 함대 밖으로 나가는 날 밟을 절차(자체 서명 신원 대신 Developer ID로)·bash
# Sign nested code first, then the app, with the runtime and a secure timestamp:
codesign --force --options runtime --timestamp \
  --sign "Developer ID Application: Example Name (EXAMPLE_TEAM)" Spark.app
ditto -c -k --keepParent Spark.app Spark.zip

# Store App Store Connect API key credentials once, in the Keychain:
xcrun notarytool store-credentials spark-notary \
  --key AuthKey_EXAMPLEKEY.p8 --key-id EXAMPLEKEY --issuer 00000000-0000-0000-0000-000000000000
xcrun notarytool submit Spark.zip --keychain-profile spark-notary --wait

xcrun stapler staple Spark.app                  # attach the ticket for offline checks
spctl --assess --type execute -vv Spark.app     # accepted, source=Notarized Developer ID

External links

Exercise

측정을 직접 다시 해봐. 함수 하나짜리 dylib을 dlopen하는 작은 probe 실행 파일을 만들어. 둘 다 자체 서명 신원으로 서명하고, probe 사본 하나는 --options runtime을 붙여 서명해. 두 probe의 CodeDirectory 플래그와 designated requirement를 적어. 그리고 둘 다 dylib을 대상으로 돌려서, 나오는 에러 문구를 그대로 옮겨 적어. 마지막으로, 가족 기본값을 짐작하지 않고 확인할 수 있게 Spark 빌드 스크립트에 검사를 하나 넣어. codesign -dv 출력을 읽어서 runtime 플래그가 보이면 실패하는 검사야.
Hint
main.c엔 <dlfcn.h>의 dlopen, dlerror, dlsym만 있으면 돼. 빌드 검사를 짤 땐, codesign -dv가 플래그를 CodeDirectory로 시작하는 줄에 찍는다는 걸 기억해. runtime은 그 줄에서만 찾아. dylib은 절대 경로로 넘겨. ./libspark.dylib처럼 주면 hardened 프로브는 "relative path not allowed in hardened program"에서 멈추고, Team ID 검사까지 가지도 않아.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.